#include "config.h"

#include "log_manager.hpp"
#include "paths.hpp"

#include <phosphor-logging/commit.hpp>
#include <sdbusplus/async.hpp>
#include <sdbusplus/server/manager.hpp>
#include <xyz/openbmc_project/Logging/Entry/client.hpp>
#include <xyz/openbmc_project/Logging/event.hpp>

#include <thread>

#include <gmock/gmock.h>
#include <gtest/gtest.h>

namespace phosphor::logging::test
{
using LoggingCleared = sdbusplus::event::xyz::openbmc_project::Logging::Cleared;
using LoggingEntry = sdbusplus::client::xyz::openbmc_project::logging::Entry<>;

// Fixture to spawn the log-manager for dbus-based testing.
class TestLogManagerDbus : public ::testing::Test
{
  protected:
    // Create the daemon and sdbusplus::async::contexts.
    void SetUp() override
    {
        // The daemon requires directories to be created first.
        std::filesystem::create_directories(phosphor::logging::paths::error());

        data = std::make_unique<fixture_data>();
    }

    // Stop the daemon, etc.
    void TearDown() override
    {
        data.reset();
    }

    /** Run a client task, wait for it to complete, and stop daemon. */
    template <typename T>
    void run(T&& t)
    {
        data->client_ctx.spawn(std::move(t) | stdexec::then([this]() {
                                   data->stop(data->client_ctx);
                               }));
        data->client_ctx.run();
    }

    // Data for the fixture.
    struct fixture_data
    {
        fixture_data() :
            client_ctx(), server_ctx(), objManager(server_ctx, OBJ_LOGGING),
            iMgr(server_ctx, OBJ_INTERNAL), mgr(server_ctx, OBJ_LOGGING, iMgr)
        {
            // Create a thread for the daemon.
            task = std::thread([this]() {
                server_ctx.request_name(BUSNAME_LOGGING);
                server_ctx.run();
            });
        }

        ~fixture_data()
        {
            // Stop the server and wait for the thread to exit.
            stop(server_ctx);
            task.join();
        }

        // Spawn a task to gracefully shutdown an sdbusplus::async::context
        static void stop(sdbusplus::async::context& ctx)
        {
            ctx.spawn(stdexec::just() |
                      stdexec::then([&ctx]() { ctx.request_stop(); }));
        }

        sdbusplus::async::context client_ctx;
        sdbusplus::async::context server_ctx;
        sdbusplus::server::manager_t objManager;
        internal::Manager iMgr;
        Manager mgr;
        std::thread task;
    };

    std::unique_ptr<fixture_data> data;

    static constexpr auto journal_unavailable = "UNAVAILABLE";
    std::string last_journal_entry()
    {
        if constexpr (LG2_COMMIT_JOURNAL)
        {
            // When running under Docker, the journal is not available and
            // sd-journal calls just silently pass.  Return a string to make
            // it obvious.
            if (!std::filesystem::exists("/run/systemd/journal/socket"))
            {
                return journal_unavailable;
            }

            sd_journal* j = nullptr;

            sd_journal_open(&j, SD_JOURNAL_LOCAL_ONLY);
            sd_journal_add_match(j, "SYSLOG_IDENTIFIER=test_manager_dbus_tests",
                                 SIZE_MAX);

            SD_JOURNAL_FOREACH_BACKWARDS(j)
            {
                const char* data = nullptr;
                size_t length = 0;

                sd_journal_get_data(j, "MESSAGE", (const void**)&data, &length);

                std::string entry(data, length);
                if (entry.contains("OPENBMC_MESSAGE_ID="))
                {
                    return entry;
                }
            }
        }

        return "";
    }
};

// Ensure we can successfully create and throw an sdbusplus event.
TEST_F(TestLogManagerDbus, GenerateSimpleEvent)
{
    EXPECT_THROW(
        { throw LoggingCleared("NUMBER_OF_LOGS", 1); }, LoggingCleared);
    return;
}

// Call the synchronous version of the commit function and verify that the
// daemon gives us a path.
TEST_F(TestLogManagerDbus, CallCommitSync)
{
    auto path = lg2::commit(LoggingCleared("NUMBER_OF_LOGS", 3));

    if constexpr (LG2_COMMIT_DBUS)
    {
        ASSERT_FALSE(path.str.empty());
        EXPECT_THAT(
            path.str,
            ::testing::StartsWith(
                std::filesystem::path(LoggingEntry::namespace_path::value) /
                LoggingEntry::namespace_path::entry));
    }

    if constexpr (LG2_COMMIT_JOURNAL)
    {
        auto entry = last_journal_entry();
        if (entry != journal_unavailable)
        {
            EXPECT_THAT(entry, ::testing::HasSubstr(
                                   "\"xyz.openbmc_project.Logging.Cleared\":"));
            EXPECT_THAT(entry, ::testing::HasSubstr("\"NUMBER_OF_LOGS\":3"));
        }
    }
}

// Call the asynchronous version of the commit function and verify that the
// metadata is saved correctly.
TEST_F(TestLogManagerDbus, CallCommitAsync)
{
    sdbusplus::message::object_path path{};
    std::string log_count{};
    pid_t pid = 0;
    std::string source_file{};

    auto create_log = [&, this]() -> sdbusplus::async::task<> {
        // Log an event.
        path = co_await lg2::commit(data->client_ctx,
                                    LoggingCleared("NUMBER_OF_LOGS", 6));

        if constexpr (LG2_COMMIT_DBUS)
        {
            // Grab the additional data.
            auto additionalData = co_await LoggingEntry(data->client_ctx)
                                      .service(Entry::default_service)
                                      .path(path.str)
                                      .additional_data();

            // Extract the NUMBER_OF_LOGS, PID, and CODE_FILE.
            for (const auto& value : additionalData)
            {
                auto getValue = [&value]() {
                    return value.substr(value.find_first_of('=') + 1);
                };

                if (value.starts_with("NUMBER_OF_LOGS="))
                {
                    log_count = getValue();
                }
                if (value.starts_with("_PID="))
                {
                    pid = std::stoull(getValue());
                }
                if (value.starts_with("_CODE_FILE="))
                {
                    source_file = getValue();
                }
            }
        }

        co_return;
    };

    run(create_log());

    if constexpr (LG2_COMMIT_DBUS)
    {
        ASSERT_FALSE(path.str.empty());
        ASSERT_FALSE(log_count.empty());

        EXPECT_THAT(
            path.str,
            ::testing::StartsWith(
                std::filesystem::path(LoggingEntry::namespace_path::value) /
                LoggingEntry::namespace_path::entry));

        EXPECT_EQ(log_count, "6");
        EXPECT_EQ(pid, getpid());
        EXPECT_EQ(source_file, std::source_location::current().file_name());
    }

    if constexpr (LG2_COMMIT_JOURNAL)
    {
        auto entry = last_journal_entry();
        if (entry != journal_unavailable)
        {
            EXPECT_THAT(entry, ::testing::HasSubstr(
                                   "\"xyz.openbmc_project.Logging.Cleared\":"));
            EXPECT_THAT(entry, ::testing::HasSubstr("\"NUMBER_OF_LOGS\":6"));
        }
    }
}

} // namespace phosphor::logging::test