// Copyright (C) 2014-2019 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 <cc/data.h>
#include <process/log_parser.h>
#include <process/process_messages.h>
#include <exceptions/exceptions.h>
#include <log/logger_support.h>
#include <process/d_log.h>
#include <testutils/io_utils.h>

#include <gtest/gtest.h>

using namespace isc;
using namespace isc::process;
using namespace isc::data;

namespace {

/// @brief Logging Test Fixture Class
///
/// Trivial class that ensures that the logging is reset to its defaults after
/// each test.  Strictly speaking this only resets the testing root logger (which
/// has the name "kea") but as the only other logger mentioned here ("wombat")
/// is not used elsewhere, that is sufficient.
class LoggingTest : public ::testing::Test {
    public:
        /// @brief Constructor
        LoggingTest() {}

        /// @brief Destructor
        ///
        /// Reset root logger back to defaults.
        ~LoggingTest() {
            isc::log::initLogger();
            wipeFiles();
        }

    /// @brief Generates a log file name suffixed with a rotation number
    /// @param rotation number to the append to the end of the file
    std::string logName(int rotation) {
        std::ostringstream os;
        os << TEST_LOG_NAME << "." << rotation;
        return (os.str());
    }

    /// @brief Removes the base log file name and 1 rotation
    void wipeFiles()  {
        static_cast<void>(remove(TEST_LOG_NAME));
        for (int i = 1; i < TEST_MAX_VERS + 1; ++i) {
            static_cast<void>(remove(logName(i).c_str()));
        }

        // Remove the lock file
        std::ostringstream os;
        os << TEST_LOG_NAME << ".lock";
        static_cast<void>(remove(os.str().c_str()));
    }

    /// @brief Name of the log file
    static const char* TEST_LOG_NAME;

    /// @brief Maximum log size
    static const int TEST_MAX_SIZE;

    /// @brief Maximum rotated log versions
    static const int TEST_MAX_VERS;

};

const char* LoggingTest::TEST_LOG_NAME = "kea.test.log";
const int LoggingTest::TEST_MAX_SIZE = 204800;  // Smallest without disabling rotation
const int LoggingTest::TEST_MAX_VERS = 2;       // More than the default of 1

// Checks that the constructor is able to process specified storage properly.
TEST_F(LoggingTest, constructor) {

    ConfigPtr null_ptr;
    EXPECT_THROW(LogConfigParser parser(null_ptr), BadValue);

    ConfigPtr nonnull(new ConfigBase());

    EXPECT_NO_THROW(LogConfigParser parser(nonnull));
}

// Checks if the LogConfigParser class is able to transform JSON structures
// into Configuration usable by log4cplus. This test checks for output
// configured to stdout on debug level.
TEST_F(LoggingTest, parsingConsoleOutput) {

    const char* config_txt =
    "{ \"loggers\": ["
    "    {"
    "        \"name\": \"kea\","
    "        \"output_options\": ["
    "            {"
    "                \"output\": \"stdout\","
    "                \"flush\": true"
    "            }"
    "        ],"
    "        \"debuglevel\": 99,"
    "        \"severity\": \"DEBUG\""
    "    }"
    "]}";

    ConfigPtr storage(new ConfigBase());

    LogConfigParser parser(storage);

    // We need to parse properly formed JSON and then extract
    // "loggers" element from it. For some reason fromJSON is
    // throwing at opening square bracket
    ConstElementPtr config = Element::fromJSON(config_txt);
    config = config->get("loggers");

    EXPECT_NO_THROW(parser.parseConfiguration(config));

    ASSERT_EQ(1, storage->getLoggingInfo().size());

    EXPECT_EQ("kea", storage->getLoggingInfo()[0].name_);
    EXPECT_EQ(99, storage->getLoggingInfo()[0].debuglevel_);
    EXPECT_EQ(isc::log::DEBUG, storage->getLoggingInfo()[0].severity_);

    ASSERT_EQ(1, storage->getLoggingInfo()[0].destinations_.size());
    EXPECT_EQ("stdout" , storage->getLoggingInfo()[0].destinations_[0].output_);
    EXPECT_TRUE(storage->getLoggingInfo()[0].destinations_[0].flush_);
}

// Checks if the LogConfigParser class fails when the configuration
// lacks a severity entry.
TEST_F(LoggingTest, parsingNoSeverity) {

    const char* config_txt =
    "{ \"loggers\": ["
    "    {"
    "        \"name\": \"kea\","
    "        \"output_options\": ["
    "            {"
    "                \"output\": \"stdout\","
    "                \"flush\": true"
    "            }"
    "        ],"
    "        \"debuglevel\": 99"
    "    }"
    "]}";

    ConfigPtr storage(new ConfigBase());

    LogConfigParser parser(storage);

    // We need to parse properly formed JSON and then extract
    // "loggers" element from it. For some reason fromJSON is
    // throwing at opening square bracket
    ConstElementPtr config = Element::fromJSON(config_txt);
    config = config->get("loggers");

    EXPECT_THROW(parser.parseConfiguration(config), BadValue);
}

// Checks if the LogConfigParser class is able to transform JSON structures
// into Configuration usable by log4cplus. This test checks for output
// configured to a file on INFO level.
TEST_F(LoggingTest, parsingFile) {

    const char* config_txt =
    "{ \"loggers\": ["
    "    {"
    "        \"name\": \"kea\","
    "        \"output_options\": ["
    "            {"
    "                \"output\": \"logfile.txt\""
    "            }"
    "        ],"
    "        \"severity\": \"INFO\""
    "    }"
    "]}";

    ConfigPtr storage(new ConfigBase());

    LogConfigParser parser(storage);

    // We need to parse properly formed JSON and then extract
    // "loggers" element from it. For some reason fromJSON is
    // throwing at opening square bracket
    ConstElementPtr config = Element::fromJSON(config_txt);
    config = config->get("loggers");

    EXPECT_NO_THROW(parser.parseConfiguration(config));

    ASSERT_EQ(1, storage->getLoggingInfo().size());

    EXPECT_EQ("kea", storage->getLoggingInfo()[0].name_);
    EXPECT_EQ(0, storage->getLoggingInfo()[0].debuglevel_);
    EXPECT_EQ(isc::log::INFO, storage->getLoggingInfo()[0].severity_);

    ASSERT_EQ(1, storage->getLoggingInfo()[0].destinations_.size());
    EXPECT_EQ("logfile.txt" , storage->getLoggingInfo()[0].destinations_[0].output_);
    // Default for immediate flush is true
    EXPECT_TRUE(storage->getLoggingInfo()[0].destinations_[0].flush_);
}

// Checks if the LogConfigParser class is able to transform data structures
// into Configuration usable by log4cplus. This test checks that more than
// one logger can be configured.
TEST_F(LoggingTest, multipleLoggers) {

    const char* config_txt =
    "{ \"loggers\": ["
    "    {"
    "        \"name\": \"kea\","
    "        \"output_options\": ["
    "            {"
    "                \"output\": \"logfile.txt\","
    "                \"flush\": true"
    "            }"
    "        ],"
    "        \"severity\": \"INFO\""
    "    },"
    "    {"
    "        \"name\": \"wombat\","
    "        \"output_options\": ["
    "            {"
    "                \"output\": \"logfile2.txt\","
    "                \"flush\": false"
    "            }"
    "        ],"
    "        \"severity\": \"DEBUG\","
    "        \"debuglevel\": 99"
    "    }"
    "]}";

    ConfigPtr storage(new ConfigBase());

    LogConfigParser parser(storage);

    // We need to parse properly formed JSON and then extract
    // "loggers" element from it. For some reason fromJSON is
    // throwing at opening square bracket
    ConstElementPtr config = Element::fromJSON(config_txt);
    config = config->get("loggers");

    EXPECT_NO_THROW(parser.parseConfiguration(config));

    ASSERT_EQ(2, storage->getLoggingInfo().size());

    EXPECT_EQ("kea", storage->getLoggingInfo()[0].name_);
    EXPECT_EQ(0, storage->getLoggingInfo()[0].debuglevel_);
    EXPECT_EQ(isc::log::INFO, storage->getLoggingInfo()[0].severity_);
    ASSERT_EQ(1, storage->getLoggingInfo()[0].destinations_.size());
    EXPECT_EQ("logfile.txt" , storage->getLoggingInfo()[0].destinations_[0].output_);
    EXPECT_TRUE(storage->getLoggingInfo()[0].destinations_[0].flush_);

    EXPECT_EQ("wombat", storage->getLoggingInfo()[1].name_);
    EXPECT_EQ(99, storage->getLoggingInfo()[1].debuglevel_);
    EXPECT_EQ(isc::log::DEBUG, storage->getLoggingInfo()[1].severity_);
    ASSERT_EQ(1, storage->getLoggingInfo()[1].destinations_.size());
    EXPECT_EQ("logfile2.txt" , storage->getLoggingInfo()[1].destinations_[0].output_);
    EXPECT_FALSE(storage->getLoggingInfo()[1].destinations_[0].flush_);
}

// Checks if the LogConfigParser class is able to transform data structures
// into Configuration usable by log4cplus. This test checks that more than
// one logging destination can be configured.
TEST_F(LoggingTest, multipleLoggingDestinations) {

    const char* config_txt =
    "{ \"loggers\": ["
    "    {"
    "        \"name\": \"kea\","
    "        \"output_options\": ["
    "            {"
    "                \"output\": \"logfile.txt\""
    "            },"
    "            {"
    "                \"output\": \"stdout\""
    "            }"
    "        ],"
    "        \"severity\": \"INFO\""
    "    }"
    "]}";

    ConfigPtr storage(new ConfigBase());

    LogConfigParser parser(storage);

    // We need to parse properly formed JSON and then extract
    // "loggers" element from it. For some reason fromJSON is
    // throwing at opening square bracket
    ConstElementPtr config = Element::fromJSON(config_txt);
    config = config->get("loggers");

    EXPECT_NO_THROW(parser.parseConfiguration(config));

    ASSERT_EQ(1, storage->getLoggingInfo().size());

    EXPECT_EQ("kea", storage->getLoggingInfo()[0].name_);
    EXPECT_EQ(0, storage->getLoggingInfo()[0].debuglevel_);
    EXPECT_EQ(isc::log::INFO, storage->getLoggingInfo()[0].severity_);
    ASSERT_EQ(2, storage->getLoggingInfo()[0].destinations_.size());
    EXPECT_EQ("logfile.txt" , storage->getLoggingInfo()[0].destinations_[0].output_);
    EXPECT_TRUE(storage->getLoggingInfo()[0].destinations_[0].flush_);
    EXPECT_EQ("stdout" , storage->getLoggingInfo()[0].destinations_[1].output_);
    EXPECT_TRUE(storage->getLoggingInfo()[0].destinations_[1].flush_);
}

// Verifies that log rotation occurs when configured.  We do not
// worry about contents of the log files, only that rotation occurs.
// Such details are tested in lib/log.  This test verifies that
// we can correcty configure logging such that rotation occurs as
// expected.
TEST_F(LoggingTest, logRotate) {
    wipeFiles();

    std::ostringstream os;
    os <<
        "{ \"loggers\": ["
        "    {"
        "        \"name\": \"kea\","
        "        \"output_options\": ["
        "            {"
        "                \"output\": \""
        << TEST_LOG_NAME << "\","  <<
        "                \"flush\": true,"
        "                \"maxsize\":"
        << TEST_MAX_SIZE << "," <<
        "                \"maxver\":"
        << TEST_MAX_VERS <<
        "            }"
        "        ],"
        "        \"debuglevel\": 99,"
        "        \"severity\": \"DEBUG\""
        "    }"
        "]}";

    // Create our server config container.
    ConfigPtr server_cfg(new ConfigBase());

    // LogConfigParser expects a list of loggers, so parse
    // the JSON text and extract the "loggers" element from it
    ConstElementPtr config = Element::fromJSON(os.str());
    config = config->get("loggers");

    // Parse the config and then apply it.
    LogConfigParser parser(server_cfg);
    ASSERT_NO_THROW(parser.parseConfiguration(config));
    ASSERT_NO_THROW(server_cfg->applyLoggingCfg());

    EXPECT_EQ(TEST_MAX_SIZE, server_cfg->getLoggingInfo()[0].destinations_[0].maxsize_);
    EXPECT_EQ(TEST_MAX_VERS, server_cfg->getLoggingInfo()[0].destinations_[0].maxver_);

    // Make sure we have the initial log file.
    ASSERT_TRUE(isc::test::fileExists(TEST_LOG_NAME));

    // Now generate a log we know will be large enough to force a rotation.
    // We borrow a one argument log message for the test.
    std::string big_arg(TEST_MAX_SIZE, 'x');
    isc::log::Logger logger("kea");

    for (int i = 1; i < TEST_MAX_VERS + 1; i++) {
        // Output the big log and make sure we get the expected rotation file.
        LOG_INFO(logger, DCTL_CONFIG_COMPLETE).arg(big_arg);
        EXPECT_TRUE(isc::test::fileExists(logName(i).c_str()));
    }

    // Clean up.
    wipeFiles();
}

/// @todo Add tests for malformed logging configuration

/// @todo There is no easy way to test applyConfiguration() and defaultLogging().
/// To test them, it would require instrumenting log4cplus to actually fake
/// the logging set up. Alternatively, we could develop set of test suites
/// that check each logging destination separately (e.g. configure log file, then
/// check if the file is indeed created or configure stdout destination, then
/// swap console file descriptors and check that messages are really logged.

};
