/*
 * Copyright 2019 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <buildboxcommon_logging.h>
#include <buildboxcommon_systemutils.h>
#include <buildboxcommon_temporarydirectory.h>
#include <buildboxcommon_temporaryfile.h>

#include <gtest/gtest.h>

#include <fstream>

#include <filesystem>

#include <sys/types.h>
#include <unistd.h>

using namespace buildboxcommon;

std::string
simulateProgramLoggingAtLevel(const buildboxcommon::LogLevel maxLevel)
{
    // The child process will redirect stderr to this file.
    buildboxcommon::TemporaryFile outputFile;
    outputFile.close();

    const pid_t pid = fork();
    if (pid == -1) {
        throw std::system_error(errno, std::system_category(),
                                "fork() failed");
    }
    if (pid == 0) {
        buildboxcommon::SystemUtils::redirectStandardOutputToFile(
            STDERR_FILENO, outputFile.name());

        auto &loggerInstance = logging::Logger::getLoggerInstance();
        const auto programName = "test-program" + std::to_string(maxLevel);
        loggerInstance.initialize(programName.c_str());

        BUILDBOX_LOG_SET_LEVEL(maxLevel);

        BUILDBOX_LOG_INFO("IMsg");
        BUILDBOX_LOG_WARNING("WMsg");
        BUILDBOX_LOG_ERROR("EMsg");
        BUILDBOX_LOG_DEBUG("DMsg");
        BUILDBOX_LOG_TRACE("TMsg");

        exit(0);
    }
    else {
        buildboxcommon::SystemUtils::waitPid(pid);
        return buildboxcommon::FileUtils::getFileContents(outputFile.name());
    }
}

TEST(LoggingTest, PrintableCommandLineTest)
{
    std::vector<std::string> command{"these", " ", "be", "seperate"};
    std::string result = logging::printableCommandLine(command);
    std::string expected = "these   be seperate";
    EXPECT_EQ(result, expected);
}

TEST(LoggingTest, PrintableCommandLineTestEmpty)
{
    std::vector<std::string> command{};
    std::string result = logging::printableCommandLine(command);
    std::string expected = "";
    EXPECT_EQ(result, expected);
}

ssize_t numberOfLines(const std::string &s)
{
    return std::count(s.cbegin(), s.cend(), '\n');
}

bool containsAllSubstrings(const std::string &s,
                           const std::vector<std::string> &substrings)
{
    return std::all_of(substrings.cbegin(), substrings.cend(),
                       [&s](const std::string &substring) {
                           return s.find(substring) != std::string::npos;
                       });
}

TEST(LoggingTest, TestSetLogLevel)
{
    const auto infoOutput =
        simulateProgramLoggingAtLevel(buildboxcommon::LogLevel::INFO);
    EXPECT_EQ(numberOfLines(infoOutput), 3);
    EXPECT_TRUE(containsAllSubstrings(infoOutput, {"IMsg", "WMsg", "EMsg"}));

    const auto warningOutput =
        simulateProgramLoggingAtLevel(buildboxcommon::LogLevel::WARNING);
    EXPECT_EQ(numberOfLines(warningOutput), 2);
    EXPECT_TRUE(containsAllSubstrings(warningOutput, {"WMsg", "EMsg"}));

    const auto errorOutput =
        simulateProgramLoggingAtLevel(buildboxcommon::LogLevel::ERROR);
    EXPECT_EQ(numberOfLines(errorOutput), 1);
    EXPECT_TRUE(containsAllSubstrings(errorOutput, {"EMsg"}));

    const auto debugOutput =
        simulateProgramLoggingAtLevel(buildboxcommon::LogLevel::DEBUG);
    EXPECT_EQ(numberOfLines(debugOutput), 4);
    EXPECT_TRUE(
        containsAllSubstrings(debugOutput, {"IMsg", "WMsg", "EMsg", "DMsg"}));

    const auto traceOutput =
        simulateProgramLoggingAtLevel(buildboxcommon::LogLevel::TRACE);
    EXPECT_EQ(numberOfLines(traceOutput), 5);
    EXPECT_TRUE(containsAllSubstrings(
        traceOutput, {"IMsg", "WMsg", "EMsg", "DMsg", "TMsg"}));
}

TEST(LoggingTest, TestLogToBothStderrAndFile)
{
    buildboxcommon::TemporaryDirectory logdir;
    buildboxcommon::TemporaryFile outputFile;
    outputFile.close();

    const pid_t pid = fork();
    if (pid == -1) {
        throw std::system_error(errno, std::system_category(),
                                "fork() failed");
    }
    if (pid == 0) {
        buildboxcommon::SystemUtils::redirectStandardOutputToFile(
            STDERR_FILENO, outputFile.name());

        // Create a temporary directory to store the log file.
        auto &loggerInstance = logging::Logger::getLoggerInstance();
        loggerInstance.setOutputDirectory(logdir.name());
        loggerInstance.enableLoggingBothStderrAndFiles();
        const auto programName = "test-program";
        loggerInstance.initialize(programName);
        BUILDBOX_LOG_ERROR("EMsg");
        exit(0);
    }
    else {
        buildboxcommon::SystemUtils::waitPid(pid);
        // Check that the log file was created.
        std::vector<std::string> fileList;
        const std::filesystem::path logDirPath(logdir.name());
        if (std::filesystem::is_directory(logDirPath)) {
            for (const auto &entry :
                 std::filesystem::directory_iterator(logDirPath)) {
                const std::string fileName = entry.path().filename().string();
                // Find the filename containing log.
                if (fileName.find(".log.") != std::string::npos) {
                    fileList.push_back(fileName);
                    auto logFileContents =
                        buildboxcommon::FileUtils::getFileContents(
                            entry.path().c_str());
                    assert(logFileContents.find("EMsg") != std::string::npos);
                    break;
                }
            }
        }
        else {
            throw std::runtime_error("Failed to open directory");
        }
        assert(fileList.size() == 1);
        auto stdErrContents =
            buildboxcommon::FileUtils::getFileContents(outputFile.name());
        assert(stdErrContents.find("EMsg") != std::string::npos);
    }
}
