/*
 * Copyright (C) 2021 Codership Oy <info@codership.com>
 *
 * This file is part of wsrep-lib.
 *
 * Wsrep-lib is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 2 of the License, or
 * (at your option) any later version.
 *
 * Wsrep-lib is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with wsrep-lib.  If not, see <https://www.gnu.org/licenses/>.
 */

#include "wsrep/reporter.hpp"

#include <boost/test/unit_test.hpp>

#include <boost/json/src.hpp>

#include <deque>
#include <fstream>
#include <iomanip>
#include <vector>

#include <unistd.h> // unlink() for cleanup

namespace json = boost::json;

//////// HELPERS ///////

static inline double
timestamp()
{
    struct timespec time;
    clock_gettime(CLOCK_REALTIME, &time);
    return (double(time.tv_sec) + double(time.tv_nsec)*1.0e-9);
}

static std::string make_progress_string(int const from, int const to,
                                        int const total,int const done,
                                        int const indefinite)
{
    std::ostringstream os;

    os << "{ \"from\": "     << from       << ", "
       << "\"to\": "         << to         << ", "
       << "\"total\": "      << total      << ", "
       << "\"done\": "       << done       << ", "
       << "\"indefinite\": " << indefinite << " }";

    return os.str();
}


static json::value
read_file(const char* const filename)
{
    std::ifstream       input(filename, std::ios::binary);
    std::vector<char>   buffer(std::istreambuf_iterator<char>(input), {});
    json::stream_parser parser;
    json::error_code    err;

    parser.write(buffer.data(), buffer.size(), err);
    if (err)
    {
        assert(0);
        return nullptr;
    }

    parser.finish(err);
    if (err)
    {
        assert(0);
        return nullptr;
    }

    return parser.release();
}

struct logs
{
    std::deque<double>      tstamp_;
    std::deque<std::string> msg_;
};

struct progress
{
    int from_;
    int to_;
    int total_;
    int done_;
    int indefinite_;

    bool indefinite() const { return total_ == indefinite_; }
    bool steady()     const { return total_ == 0; }
    progress(int const from, int const to, int const total, int const done,
        int const indefinite)
        : from_(from)
        , to_(to)
        , total_(total)
        , done_(done)
        , indefinite_(indefinite)
    {}
};

static bool
operator==(const progress& left, const progress& right)
{
    if (left.indefinite() && right.indefinite()) return true;
    if (left.steady() && right.steady()) return true;

    return (left.from_       == right.from_ &&
            left.to_         == right.to_ &&
            left.total_      == right.total_ &&
            left.done_       == right.done_ &&
            left.indefinite_ == right.indefinite_);
}

static bool
operator!=(const progress& left, const progress& right)
{
    return !(left == right);
}

static std::ostream&
operator<<(std::ostream& os, const progress& p)
{
    os << make_progress_string(p.from_, p.to_, p.total_, p.done_,p.indefinite_);
    return os;
}

struct result
{
    logs errors_;
    logs warnings_;
    struct
    {
        std::string state_;
        std::string comment_;
        progress    progress_;
    } status_;
};

static void
parse_result(const json::value& value, struct result& res,
             const std::string& path = "")
{
    //std::cout << "Parsing " << path << ": " << value << ": " << value.kind() << std::endl;
    switch (value.kind())
    {
    case json::kind::object:
    {
        auto const obj(value.get_object());
        if (!obj.empty())
        {
            for (auto it = obj.begin(); it != obj.end(); ++it)
            {
                std::string const key(it->key().data(), it->key().length());
                parse_result(it->value(), res, path + "." + key);
            }
        }
        return;
    }
    case json::kind::array:
    {
        auto const arr(value.get_array());
        if (!arr.empty())
        {
            for (auto it = arr.begin(); it != arr.end(); ++it)
            {
                parse_result(*it, res, path + ".[]");
            }
        }
        return;
    }
    case json::kind::string:
    {
        auto const val(value.get_string().c_str());
        if (path == ".errors.[].msg")
        {
            res.errors_.msg_.push_back(val);
        }
        else if (path == ".warnings.[].msg")
        {
            res.warnings_.msg_.push_back(val);
        }
        else if (path == ".status.state")
        {
            res.status_.state_ = val;
        }
        else if (path == ".status.comment")
        {
            res.status_.comment_ = val;
        }
        return;
    }
    case json::kind::uint64:
        WSREP_FALLTHROUGH;
    case json::kind::int64:
        if (path == ".status.progress.from")
        {
            res.status_.progress_.from_ = int(value.get_int64());
        }
        else if (path == ".status.progress.to")
        {
            res.status_.progress_.to_ = int(value.get_int64());
        }
        else if (path == ".status.progress.total")
        {
            res.status_.progress_.total_ = int(value.get_int64());
        }
        else if (path == ".status.progress.done")
        {
            res.status_.progress_.done_ = int(value.get_int64());
        }
        else if (path == ".status.progress.indefinite")
        {
            res.status_.progress_.indefinite_ = int(value.get_int64());
        }
        return;
    case json::kind::double_:
        if (path == ".errors.[].timestamp")
        {
            res.errors_.tstamp_.push_back(value.get_double());
        }
        else if (path == ".warnings.[].timestamp")
        {
            res.warnings_.tstamp_.push_back(value.get_double());
        }

        return;
    case json::kind::bool_:
        return;
    case json::kind::null:
        return;
    }

    assert(0);
}

static bool
equal(const std::string& left, const std::string& right)
{
    return left == right;
}

static bool
equal(double const left, double const right)
{
    return ::fabs(left - right) < 0.0001; // we are looking for ms precision
}

template <typename T>
static bool
operator!=(const std::deque<T>& left, const std::deque<T>& right)
{
    if (left.size() != right.size()) return true;

    for (size_t i(0); i < left.size(); ++i)
        if (!equal(left[i], right[i])) return true;

    return false;
}

static bool
operator!=(const logs& left, const logs& right)
{
    if (left.tstamp_ != right.tstamp_) return true;
    return (left.msg_ != right.msg_);
}

static bool
operator==(const result& left, const result& right)
{
    if (left.errors_   != right.errors_)   return false;
    if (left.warnings_ != right.warnings_) return false;

    if (left.status_.state_    != right.status_.state_)    return false;
    if (left.status_.comment_  != right.status_.comment_)  return false;
    if (left.status_.progress_ != right.status_.progress_) return false;

    return true;
}

template <typename T>
static void
print_deque(std::ostream& os,const std::deque<T> left,const std::deque<T> right)
{
    auto const max(std::max(left.size(), right.size()));
    for (size_t i(0); i < max; ++i)
    {
        os << "|\t'";

        if (i < left.size())
            os << left[i] << "'";
        else
            os << "'\t";

        if (i < right.size())
            os << "\t'" << right[i] << "'";
        else
            os << "\t''";

        if (!equal(left[i], right[i])) os << "\t!!!";

        os << "\n";
    }
}

static void
print_logs(std::ostream& os, const logs& left, const logs& right)
{
    os << "|\t" << left.msg_.size() << "\t" << right.msg_.size() << "\n";
    print_deque(os, left.tstamp_, right.tstamp_);
    print_deque(os, left.msg_, right.msg_);
}

// print two results against each other
template <typename Iteration>
static std::string
print(const result& left, const result& right, Iteration it)
{
    std::ostringstream os;

    os << std::showpoint << std::setprecision(18);

    os << "Iteration " << it << "\nerrors:\n";
    print_logs(os, left.errors_, right.errors_);
    os << "warnings:\n";
    print_logs(os, left.warnings_, right.warnings_);
    os << "state:\n";
    os << "\t" << left.status_.state_   << "\t" << right.status_.state_
       << "\n";
    os << "\t" << left.status_.comment_ << "\t" << right.status_.comment_
       << "\n";
    os << "\t" << left.status_.progress_ << "\t" << right.status_.progress_
       << "\n";

    return os.str();
}

#define VERIFY_RESULT(left, right, it)                          \
    BOOST_CHECK_MESSAGE(left == right, print(left, right, it));

static const char*
const REPORT = "report.json";

static progress
const indefinite(-1, -1, -1, -1, -1);

static progress
const steady(-1, -1, 0, 0, -1);

static struct logs
const LOGS_INIT = { std::deque<double>(), std::deque<std::string>() };
static struct result
const RES_INIT = { LOGS_INIT, LOGS_INIT,
                  { "DISCONNECTED", "Disconnected", indefinite } };

template <typename Iteration>
static void
test_log(const char* const  fname,
         wsrep::reporter&   rep,
         result&            check,
         wsrep::reporter::log_level const lvl,
         double const       tstamp,
         const std::string& msg,
         Iteration const       iteration)
{
    // this is implementaiton detail, if it changes in the code, it needs
    // to be changed here
    size_t const MAX_ERROR(4);

    logs& log(lvl == wsrep::reporter::error ? check.errors_ : check.warnings_);
    log.tstamp_.push_back(tstamp);
    if (log.tstamp_.size() > MAX_ERROR) log.tstamp_.pop_front();
    log.msg_.push_back(msg);
    if (log.msg_.size() > MAX_ERROR) log.msg_.pop_front();

    rep.report_log_msg(lvl, msg, tstamp);

    auto value = read_file(fname);
    auto res = RES_INIT;
    parse_result(value, res);
    VERIFY_RESULT(res, check, iteration);
}

static size_t const MAX_MSG = 4;

struct reporter_fixture
{
    wsrep::default_mutex mutex{};
    wsrep::reporter rep{mutex, REPORT, MAX_MSG};
};

BOOST_FIXTURE_TEST_CASE(log_msg_test, reporter_fixture)
{
    auto value = read_file(REPORT);
    BOOST_REQUIRE(value != nullptr);

    struct result res(RES_INIT), check(RES_INIT);
    parse_result(value, res);
    VERIFY_RESULT(res, check, -1);

    struct entry
    {
        double tstamp_;
        std::string msg_;
    };
    std::vector<entry> msgs =
        {
            { 0.1, "a"      },
            { 0.2, "bb"     },
            { 0.3, "ccc"    },
            { 0.4, "dddd"   },
            { 0.5, "eeeee"  },
            { 0.6, "ffffff" }
        };
    for (size_t i(0); i < msgs.size(); ++i)
    {
        test_log(REPORT, rep, check,
                 wsrep::reporter::error, msgs[i].tstamp_, msgs[i].msg_, i);
        test_log(REPORT, rep, check,
                 wsrep::reporter::warning, msgs[i].tstamp_, msgs[i].msg_, i);
    }

    // test indefinite timestmap
    std::string const msg("err");
    rep.report_log_msg(wsrep::reporter::error, msg);
    value = read_file(REPORT);
    res = RES_INIT;
    parse_result(value, res);
    BOOST_REQUIRE(res.errors_.tstamp_.back() > 0);
    BOOST_REQUIRE(res.errors_.msg_.back() == msg);

    ::unlink(REPORT);
}

BOOST_FIXTURE_TEST_CASE(state_test, reporter_fixture)
{
    using wsrep::server_state;

    double const      err_tstamp(timestamp());
    std::string const err_msg("Error!");

    struct test
    {
        struct
        {
            enum wsrep::server_state::state state;
            float progress;
        } input;
        struct result output;
    };

    logs const ERRS_INIT = { {err_tstamp}, {err_msg} };

    std::vector<test> tests =
    {
        {{ server_state::s_disconnected,  0 },
         { ERRS_INIT, LOGS_INIT,
          { "DISCONNECTED",  "Disconnected",    indefinite }}},
        {{ server_state::s_initializing,  0 },
         { ERRS_INIT, LOGS_INIT,
          { "DISCONNECTED",  "Initializing",    indefinite }}},
        {{ server_state::s_initialized,   0 },
         { ERRS_INIT, LOGS_INIT,
          { "DISCONNECTED",  "Connecting",      indefinite }}},
        {{ server_state::s_connected,     0 },
         { ERRS_INIT, LOGS_INIT,
          { "CONNECTED",     "Waiting",         indefinite }}},
        {{ server_state::s_joiner,        0 },
         { ERRS_INIT, LOGS_INIT,
          { "JOINING",       "Receiving state", indefinite }}},
        {{ server_state::s_disconnecting, 0 },
         { ERRS_INIT, LOGS_INIT,
          { "DISCONNECTING", "Disconnecting",   indefinite }}},
        {{ server_state::s_disconnected,  0 },
         { ERRS_INIT, LOGS_INIT,
          { "DISCONNECTED",  "Disconnected",    indefinite }}},
        {{ server_state::s_connected,     0 },
         { ERRS_INIT, LOGS_INIT,
          { "CONNECTED",     "Waiting",         indefinite }}},
        {{ server_state::s_joiner,        0 },
         { ERRS_INIT, LOGS_INIT,
          { "JOINING",       "Receiving SST",   indefinite }}},
        {{ server_state::s_initializing,  0 },
         { ERRS_INIT, LOGS_INIT,
          { "JOINING",       "Initializing",    indefinite }}},
        {{ server_state::s_initialized,   0 },
         { ERRS_INIT, LOGS_INIT,
          { "JOINING",       "Receiving IST",   indefinite }}},
        {{ server_state::s_joined,        0 },
         { ERRS_INIT, LOGS_INIT,
          { "JOINED",        "Syncing",         indefinite }}},
        {{ server_state::s_synced,        0 },
         { ERRS_INIT, LOGS_INIT,
          { "SYNCED",        "Operational",     steady     }}},
        {{ server_state::s_donor,         0 },
         { ERRS_INIT, LOGS_INIT,
          { "DONOR",         "Donating SST",    indefinite }}},
        {{ server_state::s_joined,        0 },
         { ERRS_INIT, LOGS_INIT,
          { "JOINED",        "Syncing",         indefinite }}},
        {{ server_state::s_synced,        0 },
         { ERRS_INIT, LOGS_INIT,
          { "SYNCED",        "Operational",     steady     }}},
        {{ server_state::s_disconnecting, 0 },
         { ERRS_INIT, LOGS_INIT,
          { "DISCONNECTING", "Disconnecting",   indefinite }}},
    };

    rep.report_log_msg(wsrep::reporter::error, err_msg, err_tstamp);

    for (auto i(tests.begin()); i != tests.end(); ++i)
    {
        rep.report_state(i->input.state);
        auto value = read_file(REPORT);
        result res(RES_INIT);
        parse_result(value, res);
        auto check(i->output);
        VERIFY_RESULT(res, check, i - tests.begin());
    }

    ::unlink(REPORT);
}

BOOST_FIXTURE_TEST_CASE(progress_test, reporter_fixture)
{
    using wsrep::server_state;

    wsrep::default_mutex m;
    wsrep::reporter   rep(m, REPORT, MAX_MSG);
    double const      warn_tstamp(timestamp());
    std::string const warn_msg("Warn!");

    static progress const progress5_0(-1, -1, 5, 0, -1);
    static progress const progress5_2(-1, -1, 5, 2, -1);
    static progress const progress5_5(-1, -1, 5, 5, -1);

    struct test
    {
        struct
        {
            enum wsrep::server_state::state state;
            int total;
            int done;
        } input;
        struct result output;
    };

    logs const WARN_INIT = { {warn_tstamp}, {warn_msg} };

    std::vector<test> tests =
    {
        {{ server_state::s_initialized,  -1, -1 },
         { LOGS_INIT, WARN_INIT,
          { "DISCONNECTED", "Connecting",      indefinite  }}},
        {{ server_state::s_connected,    -1, -1 },
         { LOGS_INIT, WARN_INIT,
          { "CONNECTED",    "Waiting",         indefinite  }}},
        {{ server_state::s_joiner,        5,  0 },
         { LOGS_INIT, WARN_INIT,
          { "JOINING",      "Receiving state", progress5_0 }}},
        {{ server_state::s_joiner,        5,  2 },
         { LOGS_INIT, WARN_INIT,
          { "JOINING",      "Receiving state", progress5_2 }}},
        {{ server_state::s_disconnected, -1, -1 },
         { LOGS_INIT, WARN_INIT,
          { "DISCONNECTED", "Disconnected",    indefinite  }}},
        {{ server_state::s_connected,    -1, -1 },
         { LOGS_INIT, WARN_INIT,
          { "CONNECTED",    "Waiting",         indefinite  }}},
        {{ server_state::s_joiner,        5,  0 },
         { LOGS_INIT, WARN_INIT,
          { "JOINING",      "Receiving SST",   progress5_0 }}},
        {{ server_state::s_joiner,        5,  2 },
         { LOGS_INIT, WARN_INIT,
          { "JOINING",      "Receiving SST",   progress5_2 }}},
        {{ server_state::s_joiner,        5,  5 },
         { LOGS_INIT, WARN_INIT,
          { "JOINING",      "Receiving SST",   progress5_5 }}},
        {{ server_state::s_initializing, -1, -1 },
         { LOGS_INIT, WARN_INIT,
          { "JOINING",      "Initializing",    indefinite  }}},
        {{ server_state::s_initializing,  5,  2 },
         { LOGS_INIT, WARN_INIT,
          { "JOINING",      "Initializing",    progress5_2 }}},
        {{ server_state::s_initialized,   5,  0 },
         { LOGS_INIT, WARN_INIT,
          { "JOINING",      "Receiving IST",   progress5_0 }}},
        {{ server_state::s_initialized,   5, 5 },
         { LOGS_INIT, WARN_INIT,
          { "JOINING",      "Receiving IST",   progress5_5 }}},
        {{ server_state::s_joined,       -1, -1 },
         { LOGS_INIT, WARN_INIT,
          { "JOINED",       "Syncing",         indefinite  }}},
        {{ server_state::s_joined,        5, 2 },
         { LOGS_INIT, WARN_INIT,
          { "JOINED",       "Syncing",         progress5_2 }}},
        {{ server_state::s_synced,       -1, -1 },
         { LOGS_INIT, WARN_INIT,
          { "SYNCED",       "Operational",     steady      }}},
        {{ server_state::s_donor,        -1, -1 },
         { LOGS_INIT, WARN_INIT,
          { "DONOR",        "Donating SST",    indefinite  }}},
        {{ server_state::s_donor,         5,  0 },
         { LOGS_INIT, WARN_INIT,
          { "DONOR",        "Donating SST",    progress5_0 }}},
        {{ server_state::s_joined,       -1, -1 },
         { LOGS_INIT, WARN_INIT,
          { "JOINED",       "Syncing",         indefinite }}},
        {{ server_state::s_synced,       -1, -1 },
         { LOGS_INIT, WARN_INIT,
          { "SYNCED",       "Operational",     steady     }}},
    };

    rep.report_log_msg(wsrep::reporter::warning, warn_msg, warn_tstamp);

    for (auto i(tests.begin()); i != tests.end(); ++i)
    {
        rep.report_state(i->input.state);
        if (i->input.total >= 0)
            rep.report_progress(make_progress_string(-1, -1,
                                                 i->input.total, i->input.done,
                                                 -1));
        auto value = read_file(REPORT);
        result res(RES_INIT);
        parse_result(value, res);
        auto check(i->output);
        VERIFY_RESULT(res, check, i - tests.begin());
    }

    ::unlink(REPORT);
}

BOOST_FIXTURE_TEST_CASE(event_test, reporter_fixture)
{
    rep.report_event("{\"msg\": \"message\"}");
    auto value = read_file(REPORT);
    BOOST_REQUIRE(value.at("events").is_array());
    auto event_array = value.at("events").as_array();
    BOOST_REQUIRE(event_array.size() == 1);
    auto event = event_array[0];
    BOOST_REQUIRE(event.is_object());
    BOOST_REQUIRE(event.at("timestamp").is_double());
    BOOST_REQUIRE(event.at("event").is_object());
    BOOST_REQUIRE(event.at("event").at("msg").is_string());
    BOOST_REQUIRE(event.at("event").at("msg").as_string() == "message");
    ::unlink(REPORT);
}
