// Copyright (C) 2011-2025 Internet Systems Consortium, Inc. ("ISC")
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

#include <config.h>

#include <stdio.h>
#include <unistd.h>

#include <fstream>
#include <iostream>
#include <string>

#include <gtest/gtest.h>

#include <boost/scoped_array.hpp>
#include <boost/lexical_cast.hpp>

#include <exceptions/exceptions.h>

#include <log/macros.h>
#include <log/log_messages.h>
#include <log/logger.h>
#include <log/logger_level.h>
#include <log/logger_manager.h>
#include <log/logger_name.h>
#include <log/logger_specification.h>
#include <log/message_initializer.h>
#include <log/output_option.h>
#include <testutils/gtest_utils.h>

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

using namespace isc;
using namespace isc::log;
using namespace std;

/// \brief LoggerManager Test
class LoggerManagerTest : public ::testing::Test {
public:
    LoggerManagerTest() {
        // Initialization of logging is done in main()
    }

    ~LoggerManagerTest() {
        LoggerManager::reset();
    }
};

// Convenience class to create the specification for the logger "filelogger",
// which, as the name suggests, logs to a file.  It remembers the file name and
// deletes the file when instance of the class is destroyed.
class SpecificationForFileLogger {
public:

    // Constructor - allocate file and create the specification object
    SpecificationForFileLogger() : spec_(), name_(createTempFilename()),
                                   logname_("filelogger") {

        // Set the output to a temporary file.
        OutputOption option;
        option.destination = OutputOption::DEST_FILE;
        option.filename = name_;

        // Set target output to the file logger.  The defaults indicate
        // INFO severity.
        spec_.setName(logname_);
        spec_.addOutputOption(option);
    }

    // Destructor, remove the file.  This is only a test, so ignore failures
    ~SpecificationForFileLogger() {
        if (! name_.empty()) {
            static_cast<void>(remove(name_.c_str()));

            // Depending on the log4cplus version, a lock file may also be
            // created.
            static_cast<void>(remove((name_ + ".lock").c_str()));
        }
    }

    // Return reference to the logging specification for this loggger
    LoggerSpecification& getSpecification() {
        return spec_;
    }

    // Return name of the logger
    string getLoggerName() const {
        return logname_;
    }

    // Return name of the file
    string getFileName() const {
        return name_;
    }

    // Create temporary filename
    //
    // The compiler warns against tmpnam() and suggests mkstemp instead.
    // Unfortunately, this creates the filename and opens it.  So we need to
    // close and delete the file before returning the name.  Also, the name
    // is based on the template supplied and the name of the temporary
    // directory may vary between systems.  So translate TMPDIR and if that
    // does not exist, use /tmp.
    //
    // \return Temporary file name
    static std::string createTempFilename() {
        string filename = string(TEMP_DIR) + "/kea_logger_manager_test_XXXXXX";

        // Copy into writable storage for the call to mkstemp
        boost::scoped_array<char> tname(new char[filename.size() + 1]);
        strcpy(tname.get(), filename.c_str());

        // Create file, close and delete it, and store the name for later.
        // There is still a race condition here, albeit a small one.
        int filenum = mkstemp(tname.get());
        if (filenum == -1) {
            isc_throw(Exception, "Unable to obtain unique filename");
        }
        close(filenum);

        return (string(tname.get()));
    }


private:
    LoggerSpecification     spec_;      // Specification for this file logger
    string                  name_;      // Name of the output file
    string                  logname_;   // Name of this logger
};

// Convenience function to read an output log file and check that each line
// contains the expected message ID
//
// \param filename Name of the file to check
// \param start Iterator pointing to first expected message ID
// \param finish Iterator pointing to last expected message ID
template <typename T>
void checkFileContents(const std::string& filename, T start, T finish) {

    // Access the file for input
    ifstream infile(filename.c_str());
    if (! infile.good()) {
        FAIL() << "Unable to open the logging file " << filename;
    }

    // Iterate round the expected message IDs and check that they appear in
    // the string.
    string line;    // Line read from the file

    T i = start;    // Iterator
    getline(infile, line);
    int lineno = 1;

    while ((i != finish) && (infile.good())) {

        // Check that the message ID appears in the line.
        EXPECT_TRUE(line.find(string(*i)) != string::npos)
            << "Expected to find " << string(*i) << " on line " << lineno
            << " of logging file " << filename;

        // Go for the next line
        ++i;
        getline(infile, line);
        ++lineno;
    }

    // Why did the loop end?
    EXPECT_TRUE(i == finish) << "Did not reach the end of the message ID list";
    EXPECT_TRUE(infile.eof()) << "Did not reach the end of the logging file";

    // File will close when the instream is deleted at the end of this
    // function.
}

// Check that the logger correctly creates something logging to a file.
TEST_F(LoggerManagerTest, FileLogger) {

    // Create a specification for the file logger and use the manager to
    // connect the "filelogger" logger to it.
    SpecificationForFileLogger file_spec;

    // For the first test, we want to check that the file is created
    // if it does not already exist.  So delete the temporary file before
    // logging the first message.
    static_cast<void>(remove(file_spec.getFileName().c_str()));

    // Set up the file appenders.
    LoggerManager manager;
    manager.process(file_spec.getSpecification());

    // Try logging to the file.  Local scope is set to ensure that the logger
    // is destroyed before we reset the global logging.  We record what we
    // put in the file for a later comparison.
    vector<MessageID> ids;
    {

        // Scope-limit the logger to ensure it is destroyed after the brief
        // check.  This adds weight to the idea that the logger will not
        // keep the file open.
        Logger logger(file_spec.getLoggerName().c_str());

        LOG_FATAL(logger, LOG_DUPLICATE_MESSAGE_ID).arg("test");
        ids.push_back(LOG_DUPLICATE_MESSAGE_ID);

        LOG_FATAL(logger, LOG_DUPLICATE_NAMESPACE).arg("test");
        ids.push_back(LOG_DUPLICATE_NAMESPACE);
    }
    LoggerManager::reset();

    // At this point, the output file should contain two lines with messages
    // LOG_DUPLICATE_MESSAGE_ID and LOG_DUPLICATE_NAMESPACE messages - test this.
    checkFileContents(file_spec.getFileName(), ids.begin(), ids.end());

    // Re-open the file (we have to assume that it was closed when we
    // reset the logger - there is no easy way to check) and check that
    // new messages are appended to it.  We use the alternative
    // invocation of process() here to check it works.
    vector<LoggerSpecification> spec(1, file_spec.getSpecification());
    manager.process(spec.begin(), spec.end());

    // Create a new instance of the logger and log three more messages.
    Logger logger(file_spec.getLoggerName().c_str());

    LOG_FATAL(logger, LOG_NO_SUCH_MESSAGE).arg("test");
    ids.push_back(LOG_NO_SUCH_MESSAGE);

    LOG_FATAL(logger, LOG_INVALID_MESSAGE_ID).arg("test").arg("test2");
    ids.push_back(LOG_INVALID_MESSAGE_ID);

    LOG_FATAL(logger, LOG_NO_MESSAGE_ID).arg("42");
    ids.push_back(LOG_NO_MESSAGE_ID);

    // Close the file and check again
    LoggerManager::reset();
    checkFileContents(file_spec.getFileName(), ids.begin(), ids.end());
}

// Check if the file rolls over when it gets above a certain size.
TEST_F(LoggerManagerTest, FileSizeRollover) {
    // Set to a suitable minimum that log4cplus can cope with.
    static const size_t SIZE_LIMIT = 204800;

    // Set up the name of the file.
    SpecificationForFileLogger file_spec;
    LoggerSpecification& spec = file_spec.getSpecification();

    // Expand the option to ensure that a maximum version size is set.
    LoggerSpecification::iterator opt = spec.begin();
    EXPECT_TRUE(opt != spec.end());
    opt->maxsize = SIZE_LIMIT;    // Bytes
    opt->maxver = 2;

    // The current output file does not exist (the creation of file_spec
    // ensures that.  Check that previous versions don't either.
    vector<string> prev_name;
    for (int i = 0; i < 3; ++i) {
        prev_name.push_back(file_spec.getFileName() + "." +
                            boost::lexical_cast<string>(i + 1));
        (void) remove(prev_name[i].c_str());
    }

    // Generate an argument for a message that ensures that the message when
    // logged will be over that size.
    string big_arg(SIZE_LIMIT, 'x');

    // Set up the file logger
    LoggerManager manager;
    manager.process(spec);

    // Log the message twice using different message IDs.  This should generate
    // three files as for the log4cplus implementation, the files appear to
    // be rolled after the message is logged.
    {
        Logger logger(file_spec.getLoggerName().c_str());
        LOG_FATAL(logger, LOG_NO_SUCH_MESSAGE).arg(big_arg);
        LOG_FATAL(logger, LOG_DUPLICATE_NAMESPACE).arg(big_arg);
    }

    // Check them.
    LoggerManager::reset();     // Ensure files are closed

    vector<MessageID> ids;
    ids.push_back(LOG_NO_SUCH_MESSAGE);
    checkFileContents(prev_name[1], ids.begin(), ids.end());

    ids.clear();
    ids.push_back(LOG_DUPLICATE_NAMESPACE);
    checkFileContents(prev_name[0], ids.begin(), ids.end());

    // Log another message and check that the files have rotated and that
    // a .3 version does not exist.
    manager.process(spec);
    {
        Logger logger(file_spec.getLoggerName().c_str());
        LOG_FATAL(logger, LOG_NO_MESSAGE_TEXT).arg("42").arg(big_arg);
    }

    LoggerManager::reset();     // Ensure files are closed

    // Check that the files have moved.
    ids.clear();
    ids.push_back(LOG_DUPLICATE_NAMESPACE);
    checkFileContents(prev_name[1], ids.begin(), ids.end());

    ids.clear();
    ids.push_back(LOG_NO_MESSAGE_TEXT);
    checkFileContents(prev_name[0], ids.begin(), ids.end());

    // ... and check that the .3 version does not exist.
    ifstream file3(prev_name[2].c_str(), ifstream::in);
    EXPECT_FALSE(file3.good());

    // Tidy up
    for (vector<string>::size_type i = 0; i < prev_name.size(); ++i) {
       (void) remove(prev_name[i].c_str());
    }
}

// Check if an exception is thrown if maxsize is too large.
TEST_F(LoggerManagerTest, TooLargeMaxsize) {
    // Set up the name of the file.
    SpecificationForFileLogger file_spec;
    LoggerSpecification& spec(file_spec.getSpecification());

    // UINT64_MAX should be large enough.
    LoggerSpecification::iterator opt = spec.begin();
    EXPECT_TRUE(opt != spec.end());
    opt->maxsize = std::numeric_limits<uint64_t>::max();  // bytes

    // Set up the file logger.
    LoggerManager manager;
    EXPECT_THROW_MSG(manager.process(spec), BadValue,
                     "expected maxsize < 2147483647MB, but instead got "
                     "18446744073709MB");

    opt->maxsize = 1000000LL * (std::numeric_limits<int32_t>::max() + 1LL);  // bytes
    EXPECT_THROW_MSG(manager.process(spec), BadValue,
                     "expected maxsize < 2147483647MB, but instead got "
                     "2147483648MB");
}

namespace { // begin unnamed namespace

// When we begin to use C++11, we could replace use of POSIX API with
// <regex>.

class RegexHolder {
public:
    RegexHolder(const char* expr, const int flags = REG_EXTENDED) {
        const int rc = regcomp(&regex_, expr, flags);
        if (rc) {
            regfree(&regex_);
            throw;
        }
    }

    ~RegexHolder() {
        regfree(&regex_);
    }

    regex_t* operator*() {
        return (&regex_);
    }

private:
    regex_t regex_;
};

} // end of unnamed namespace

// Check that the logger correctly outputs the full formatted layout
// pattern.
TEST_F(LoggerManagerTest, checkLayoutPattern) {
    // Create a specification for the file logger and use the manager to
    // connect the "filelogger" logger to it.
    SpecificationForFileLogger file_spec;

    // For the first test, we want to check that the file is created
    // if it does not already exist.  So delete the temporary file before
    // logging the first message.
    static_cast<void>(remove(file_spec.getFileName().c_str()));

    // Set up the file appenders.
    LoggerManager manager;
    manager.process(file_spec.getSpecification());

    // Try logging to the file.  Local scope is set to ensure that the logger
    // is destroyed before we reset the global logging.
    {
        Logger logger(file_spec.getLoggerName().c_str());
        LOG_FATAL(logger, LOG_DUPLICATE_MESSAGE_ID).arg("test");
    }

    LoggerManager::reset();

    // Access the file for input
    const std::string& filename = file_spec.getFileName();
    ifstream infile(filename.c_str());
    if (! infile.good()) {
        FAIL() << "Unable to open the logging file " << filename;
    }

    std::string line;
    std::getline(infile, line);

    RegexHolder regex(// %D{%Y-%m-%d %H:%M:%S.%q}
                      "^[[:digit:]]{4}-[[:digit:]]{2}-[[:digit:]]{2}[[:space:]]"
                      "[[:digit:]]{2}:[[:digit:]]{2}:[[:digit:]]{2}\\.[[:digit:]]+[[:space:]]"
                      // %-5p
                      "[[:alpha:]]{1,5}[[:space:]]"
                      // [%c/%i/%t]
                      "\\[[[:alnum:]\\-\\.]+/[[:digit:]]+\\.(0x)?[[:xdigit:]]+\\][[:space:]]"
                      );

    const int re = regexec(*regex, line.c_str(), 0, NULL, 0);
    ASSERT_EQ(0, re)
        << "Logged message does not match expected layout pattern";
}

// Check that after calling the logDuplicatedMessages, the duplicated
// messages are removed.
TEST_F(LoggerManagerTest, logDuplicatedMessages) {
    // Original set should not have duplicates.
    ASSERT_EQ(0, MessageInitializer::getDuplicates().size());

    // This just defines 1, but we'll add it a number of times.
    const char* dupe[] = {
        "DUPE", "dupe",
        NULL
    };
    const MessageInitializer init_message_initializer_1(dupe);
    const MessageInitializer init_message_initializer_2(dupe);

    MessageInitializer::loadDictionary();
    // Should have a duplicate now.
    ASSERT_EQ(1, MessageInitializer::getDuplicates().size());

    // The logDuplicatedMessages, besides logging, should also remove the
    // duplicates.
    LoggerManager::logDuplicatedMessages();
    ASSERT_EQ(0, MessageInitializer::getDuplicates().size());
}

// Check that output options can be inherited.
TEST_F(LoggerManagerTest, outputOptionsInheritance) {
    LoggerManager manager;
    SpecificationForFileLogger file_spec;
    vector<LoggerSpecification> specs;

    // Create the root logger configuration with a file output option.
    string root_name(getRootLoggerName());
    LoggerSpecification root_spec(root_name);
    OutputOption root_option;
    root_option.destination = OutputOption::DEST_FILE;
    root_option.filename = file_spec.getFileName();
    root_option.pattern = "%p %m\n";
    root_spec.addOutputOption(root_option);
    specs.push_back(root_spec);

    // Create a child logger configuration without any output options.
    // It should inherit the output option from the root logger.
    string foo_name(root_name + ".foo");
    LoggerSpecification foo_spec(foo_name);
    specs.push_back(foo_spec);

    // Create another child logger configuration with a console output option.
    string bar_name(root_name + ".bar");
    LoggerSpecification bar_spec(bar_name);
    OutputOption bar_option;
    bar_option.destination = OutputOption::DEST_CONSOLE;
    bar_option.pattern = "%p %m\n";
    bar_spec.addOutputOption(bar_option);
    specs.push_back(bar_spec);

    // Check the number of output options for each specification.
    EXPECT_EQ(root_spec.optionCount(), 1);
    EXPECT_EQ(foo_spec.optionCount(), 0);
    EXPECT_EQ(bar_spec.optionCount(), 1);

    // Process all the specifications.
    manager.process(specs.begin(), specs.end());

    // Log two messages each.
    Logger root_logger(root_name.c_str());
    Logger foo_logger(foo_name.c_str());
    Logger bar_logger(bar_name.c_str());
    LOG_INFO(root_logger, "from root logger 1");
    LOG_INFO(foo_logger, "from foo logger 1");
    LOG_INFO(bar_logger, "from bar logger 1");
    LOG_INFO(root_logger, "from root logger 2");
    LOG_INFO(foo_logger, "from foo logger 2");
    LOG_INFO(bar_logger, "from bar logger 2");

    // Check that root and foo were logged to file and that bar which
    // had an explicit console configuration did not.
    std::ifstream ifs(file_spec.getFileName());
    std::stringstream s;
    s << ifs.rdbuf();
    std::string const result(s.str());
    EXPECT_NE(result.find("INFO from root logger 1"), string::npos);
    EXPECT_NE(result.find("INFO from foo logger 1"), string::npos);
    EXPECT_EQ(result.find("INFO from bar logger 1"), string::npos);
    EXPECT_NE(result.find("INFO from root logger 2"), string::npos);
    EXPECT_NE(result.find("INFO from foo logger 2"), string::npos);
    EXPECT_EQ(result.find("INFO from bar logger 2"), string::npos);
}
