#include "i_mainloop.h"
#include "mainloop.h"

#include <fcntl.h>
#include <chrono>

#include "cptest.h"
#include "config.h"
#include "config_component.h"
#include "mock/mock_time_get.h"
#include "mock/mock_environment.h"
#include "mock/mock_messaging.h"
#include "mock/mock_agent_details.h"
#include "scope_exit.h"
#include "metric/all_metric_event.h"
#include "debug.h"

using namespace std;
using namespace testing;
using namespace chrono;

USE_DEBUG_FLAG(D_MAINLOOP);

class EndTest
{
};
typedef std::chrono::duration<size_t, std::ratio<86400>> days;
class MainloopTest : public Test
{
public:
    MainloopTest()
    {
        EXPECT_CALL(mock_env, getActiveContexts()).WillRepeatedly(ReturnRef(active_context));

        Debug::setUnitTestFlag(D_MAINLOOP, Debug::DebugLevel::DEBUG);
        Debug::setNewDefaultStdout(&capture_debug);

        ON_CALL(mock_agent_details, getFogDomain()).WillByDefault(Return(Maybe<string>(string("fog_domain.com"))));
        ON_CALL(mock_agent_details, getFogPort()).WillByDefault(Return(Maybe<uint16_t>(443)));
    }

    ~MainloopTest()
    {
        Debug::setUnitTestFlag(D_MAINLOOP, Debug::DebugLevel::INFO);
        Debug::setNewDefaultStdout(&cout);
        mainloop_comp.fini();
    }

    void
    expectPersistentMessage()
    {
        EXPECT_CALL(
            mock_msg,
            sendAsyncMessage(_, "/api/v1/agents/events", _, _, _, _)
        ).Times(2).WillRepeatedly(
            WithArgs<2, 3>(
                Invoke(
                    [this](const string &req_body, MessageCategory tag)
                    {
                        EXPECT_TRUE(tag == MessageCategory::LOG || tag == MessageCategory::METRIC);
                        if (tag == MessageCategory::LOG) startup_report_body = req_body;
                        static bool should_throw = false;
                        if (should_throw) {
                            should_throw = false;
                            throw EndTest();
                        } else {
                            should_throw = true;
                        }

                        return;
                    }
                )
            )
        );
    }

    void
    setupBalancedIntervalTest(
        chrono::microseconds start_time,
        chrono::microseconds interval,
        const string &routine_name,
        chrono::microseconds offset = chrono::microseconds(0),
        chrono::microseconds time_advance = chrono::hours(24)
    )
    {
        chrono::microseconds time = start_time;
        EXPECT_CALL(mock_time, getWalltime()).WillRepeatedly(InvokeWithoutArgs([&]() { return time; }));

        EXPECT_CALL(mock_time, getMonotonicTime())
            .WillRepeatedly(InvokeWithoutArgs(
                [&]()
                {
                    auto old_time = time;
                    time += time_advance;
                    return old_time;
                }
            ));
        auto callback = [this]() { mainloop->stop(); };

        mainloop->addBalancedIntervalRoutine(
            I_MainLoop::RoutineType::RealTime, interval, callback, routine_name, offset, true
        );

        // Run the mainloop
        mainloop->run();
    }

    void
    expectWaitTimeInDebug(chrono::microseconds expected_wait_time)
    {
        string expected_debug =
            "Balanced interval routine waiting " + to_string(expected_wait_time.count()) + " microseconds";
        EXPECT_THAT(capture_debug.str(), HasSubstr(expected_debug));
    }

    I_Environment::ActiveContexts active_context;

    NiceMock<MockTimeGet>       mock_time;
    MainloopComponent           mainloop_comp;
    NiceMock<MockEnvironment>   mock_env;
    StrictMock<MockMessaging>   mock_msg;
    NiceMock<MockAgentDetails>  mock_agent_details;
    I_MainLoop                  *mainloop = Singleton::Consume<I_MainLoop>::from(mainloop_comp);
    ConfigComponent             conf;
    Config::I_Config            *config = nullptr;
    ostringstream               capture_debug;
    string                      startup_report_body;
    bool                        stop_test = false;
};


TEST_F(MainloopTest, do_nothing)
{
}

TEST_F(MainloopTest, start_with_nothing_to_do)
{
    mainloop->run();
}

TEST_F(MainloopTest, basic_metrics_check)
{
    string startup_body_sent;
    expectPersistentMessage();

    mainloop_comp.init();

    mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::RealTime,
        [this] ()
        {
            while (true) {
                mainloop->yield(true);
            }
        },
        "internal test cb",
        true
    );

    try {
        mainloop->run();
    } catch (...) {
    }
    AllMetricEvent all_mt_event;
    all_mt_event.setReset(false);
    all_mt_event.notify();


    string mainloop_str =
        "{\n"
        "    \"Metric\": \"Mainloop sleep time data\",\n"
        "    \"Reporting interval\": 600,\n"
        "    \"mainloopMaxTimeSliceSample\": 1500,\n"
        "    \"mainloopAvgTimeSliceSample\": 1500.0,\n"
        "    \"mainloopLastTimeSliceSample\": 1500,\n"
        "    \"mainloopMaxSleepTimeSample\": 1500,\n"
        "    \"mainloopAvgSleepTimeSample\": 1500.0,\n"
        "    \"mainloopLastSleepTimeSample\": 1500,\n"
        "    \"mainloopMaxStressValueSample\": 0,\n"
        "    \"mainloopAvgStressValueSample\": 0.0,\n"
        "    \"mainloopLastStressValueSample\": 0\n"
        "}";

    EXPECT_THAT(all_mt_event.performNamedQuery(), ElementsAre(Pair("Mainloop sleep time data", mainloop_str)));

    static const string expected_message =
        "{\n"
        "    \"log\": {\n"
        "        \"eventTime\": \"\",\n"
        "        \"eventName\": \"Nano service successfully started\",\n"
        "        \"eventSeverity\": \"Info\",\n"
        "        \"eventPriority\": \"High\",\n"
        "        \"eventType\": \"Event Driven\",\n"
        "        \"eventLevel\": \"Log\",\n"
        "        \"eventLogLevel\": \"info\",\n"
        "        \"eventAudience\": \"Internal\",\n"
        "        \"eventAudienceTeam\": \"\",\n"
        "        \"eventFrequency\": 0,\n"
        "        \"eventTags\": [\n"
        "            \"Informational\"\n"
        "        ],\n"
        "        \"eventSource\": {\n"
        "            \"agentId\": \"\",\n"
        "            \"eventTraceId\": \"\",\n"
        "            \"eventSpanId\": \"\",\n"
        "            \"issuingEngineVersion\": \"\",\n"
        "            \"serviceName\": \"Unnamed Nano Service\"\n"
        "        },\n"
        "        \"eventData\": {}\n"
        "    }\n"
        "}";

    EXPECT_EQ(startup_report_body, expected_message);
}

TEST_F(MainloopTest, no_sleep_time_metrics_check)
{
    mainloop_comp.preload();

    string startup_body_sent;
    expectPersistentMessage();

    chrono::microseconds time(0);
    EXPECT_CALL(
        mock_time,
        getMonotonicTime()
    ).WillRepeatedly(InvokeWithoutArgs([&]{ time += microseconds(3000); return time; } ));

    setConfiguration<int>(
        2,
        string("Mainloop"),
        string("Idle routine time slice")
    );

    mainloop_comp.init();
    mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::RealTime,
        [this] ()
        {
            while (true) {
                mainloop->yield(true);
            }
        },
        "internal test cb",
        true
    );

    try {
        mainloop->run();
    } catch (...) {
    }
    AllMetricEvent all_mt_event;
    all_mt_event.setReset(false);
    all_mt_event.notify();

    string mainloop_str =
        "{\n"
        "    \"Metric\": \"Mainloop sleep time data\",\n"
        "    \"Reporting interval\": 600,\n"
        "    \"mainloopMaxTimeSliceSample\": 2,\n"
        "    \"mainloopAvgTimeSliceSample\": 2.0,\n"
        "    \"mainloopLastTimeSliceSample\": 2,\n"
        "    \"mainloopMaxSleepTimeSample\": 0,\n"
        "    \"mainloopAvgSleepTimeSample\": 0.0,\n"
        "    \"mainloopLastSleepTimeSample\": 0,\n"
        "    \"mainloopMaxStressValueSample\": 0,\n"
        "    \"mainloopAvgStressValueSample\": 0.0,\n"
        "    \"mainloopLastStressValueSample\": 0\n"
        "}";

    EXPECT_THAT(all_mt_event.query(), ElementsAre(mainloop_str));

    static const string expected_message =
        "{\n"
        "    \"log\": {\n"
        "        \"eventTime\": \"\",\n"
        "        \"eventName\": \"Nano service successfully started\",\n"
        "        \"eventSeverity\": \"Info\",\n"
        "        \"eventPriority\": \"High\",\n"
        "        \"eventType\": \"Event Driven\",\n"
        "        \"eventLevel\": \"Log\",\n"
        "        \"eventLogLevel\": \"info\",\n"
        "        \"eventAudience\": \"Internal\",\n"
        "        \"eventAudienceTeam\": \"\",\n"
        "        \"eventFrequency\": 0,\n"
        "        \"eventTags\": [\n"
        "            \"Informational\"\n"
        "        ],\n"
        "        \"eventSource\": {\n"
        "            \"agentId\": \"\",\n"
        "            \"eventTraceId\": \"\",\n"
        "            \"eventSpanId\": \"\",\n"
        "            \"issuingEngineVersion\": \"\",\n"
        "            \"serviceName\": \"Unnamed Nano Service\"\n"
        "        },\n"
        "        \"eventData\": {}\n"
        "    }\n"
        "}";

    EXPECT_EQ(startup_report_body, expected_message);
}

TEST(MainloopTestWithoutComponent, register_config)
{
    ConfigComponent config;

    StrictMock<MockTimeGet> mock_timer;
    MainloopComponent mainloop_comp;
    ::Environment env;

    env.preload();
    mainloop_comp.preload();
    env.init();

    string config_json =
        "{\n"
        "    \"Mainloop\": {\n"
        "        \"Idle routine time slice\": [\n"
        "            {\n"
        "                \"value\": 200\n"
        "            }\n"
        "        ]\n"
        "    }\n"
        "}\n";

    istringstream ss(config_json);
    Singleton::Consume<Config::I_Config>::from(config)->loadConfiguration(ss);

    int time_slice = getConfigurationWithDefault<int>(100, "Mainloop", "Idle routine time slice");
    EXPECT_EQ(time_slice, 200);

    env.fini();
}

TEST_F(MainloopTest, call_single_cb)
{
    int num_called = 0;
    auto cb = [&num_called] () {
        num_called++;
    };

    mainloop->addOneTimeRoutine(I_MainLoop::RoutineType::RealTime, cb, "call single cb test", true);
    mainloop->run();
    EXPECT_EQ(1, num_called);
}

TEST_F(MainloopTest, call_single_yield)
{
    int num_called = 0;
    auto ml = mainloop;
    auto cb = [&num_called, ml] () {
        num_called++;
        ml->yield(true);
        num_called++;
    };

    mainloop->addOneTimeRoutine(I_MainLoop::RoutineType::RealTime, cb, "call_single_yield test", true);
    mainloop->run();
    EXPECT_EQ(2, num_called);
}

TEST_F(MainloopTest, stop_from_cb)
{
    bool dtor_called = false;
    auto stop_cb = [&dtor_called, this] () {
        auto scope_guard = make_scope_exit([&dtor_called] () { dtor_called = true; });
        mainloop->stop();
        ADD_FAILURE() << "Should stop before this";
    };

    mainloop->addOneTimeRoutine(I_MainLoop::RoutineType::RealTime, stop_cb, "stop_from_cb test", true);
    mainloop->run();
    EXPECT_TRUE(dtor_called); // Verifying that we exited the routine cleanly, invoking the dtor
}

TEST_F(MainloopTest, stop_other_cb)
{
    int num_called = 0;
    auto stoped_cb = [&num_called] () { num_called++; };
    auto stop_id = mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::Offline,
        stoped_cb,
        "stop_other_cb test - cb to stop",
        true
    );

    auto stopping_bc = [stop_id, this] () { mainloop->stop(stop_id); };
    mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::RealTime,
        stopping_bc,
        "stop_other_cb test - cb that stops",
        true
    );

    mainloop->run();
    EXPECT_EQ(0, num_called);
}

TEST_F(MainloopTest, call_recurring_cb)
{
    int num_called = 0;
    auto cb = [&num_called, this] () {
        num_called++;
        if (num_called == 3) mainloop->stop();
    };

    mainloop->addRecurringRoutine(
        I_MainLoop::RoutineType::RealTime,
        chrono::microseconds(0),
        cb,
        "call_recurring_cb",
        true
    );
    mainloop->run();
    EXPECT_EQ(3, num_called);
}

TEST_F(MainloopTest, call_file_cb)
{
    CPTestTempfile file({ "a", "b", "c" });
    int fd = open(file.fname.c_str(), O_RDONLY);
    ASSERT_LT(0, fd);

    int num_called = 0;
    auto cb = [&num_called, fd, this] () {
        char ch;
        ASSERT_EQ(1, read(fd, &ch, 1));
        if (ch == 'c') mainloop->stop();
        num_called++;
    };
    mainloop->addFileRoutine(I_MainLoop::RoutineType::RealTime, fd, cb, "call_file_cb test", true);

    mainloop->run();
    EXPECT_EQ(4, num_called);
}

TEST_F(MainloopTest, stop_while_routines_are_running)
{
    int num_called = 0;
    auto routine = [&num_called] () {
        num_called++;
    };

    auto stop_cb = [this] () {
        mainloop->stopAll();
    };

    mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::RealTime,
        routine,
        "stop_while_routines_are_running test - cb to stop",
        true
    );
    mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::Timer,
        stop_cb,
        "stop_while_routines_are_running test - cb that stops",
        true
    );
    mainloop->run();
    // "routine" is of higher priority than "stop", so it should run at least once before "stop" is called.
    EXPECT_LT(0, num_called);
}

TEST_F(MainloopTest, halt_self)
{
    int num_called = 0;
    auto routine = [&num_called, this] () {
        mainloop->halt();
        num_called++;
    };
    mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::RealTime,
        routine,
        "halt_self test - cb to stop",
        true
    );

    auto stop_cb = [this] () {
        mainloop->stopAll();
    };
    mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::RealTime,
        stop_cb,
        "halt_self test - cb that stops",
        true
    );

    mainloop->run();
    EXPECT_EQ(0, num_called);
}

TEST_F(MainloopTest, halt_resume)
{
    auto stop_cb = [this] () {
        mainloop->stopAll();
    };
    auto id = mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::Timer,
        stop_cb,
        "halt_resume test - cb that stops",
        true
    );

    int num_called = 0;
    auto routine = [&num_called, this, id] () {
        mainloop->halt(id);

        while (true) {
            if (num_called == 100)  mainloop->resume(id);
            num_called++;
            mainloop->yield(true);
        }
    };
    mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::RealTime,
        routine,
        "halt_resume test - cb to stop",
        true
    );

    mainloop->run();
    EXPECT_LT(100, num_called);
}

TEST_F(MainloopTest, death_on_run_twice)
{
    cptestPrepareToDie();
    auto cb = [this] () {
        EXPECT_DEATH(mainloop->run(), "MainloopComponent::Impl::run was called while it was already running");
    };
    mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::RealTime,
        cb,
        "death_on_run_twice test",
        true
    );

    mainloop->run();
}

TEST_F(MainloopTest, get_routine_id)
{
    cptestPrepareToDie();
    auto cb = [this] () {
        EXPECT_EQ(mainloop->getCurrentRoutineId().unpack(), 1u);
        EXPECT_DEATH(mainloop->run(), "MainloopComponent::Impl::run was called while it was already running");
    };
    mainloop->addOneTimeRoutine(
        I_MainLoop::RoutineType::RealTime,
        cb,
        "get_routine_id test",
        true
    );

    mainloop->run();
}

TEST_F(MainloopTest, check_routine_name)
{
    int num_called = 0;
    auto cb = [&num_called] () {
        num_called++;
    };
    Debug::setUnitTestFlag(D_MAINLOOP, Debug::DebugLevel::TRACE);
    mainloop->addOneTimeRoutine(I_MainLoop::RoutineType::RealTime, cb, "check routine name test", true);
    EXPECT_THAT(capture_debug.str(), HasSubstr("Added new routine. Name: check routine name test"));
    mainloop->run();
    EXPECT_THAT(
        capture_debug.str(),
        HasSubstr("Starting execution of corutine. Routine named: check routine name test")
    );
}

TEST_F(MainloopTest, balanced_interval_empty_routine_name_hour_start)
{
    Debug::setUnitTestFlag(D_MAINLOOP, Debug::DebugLevel::TRACE);

    // Start on day 1 at 4:00 (at the start of the hour)
    // Interval is 2 hours
    // Because routine_name is empty, no shifting will be done so remaining time will be exactly equal to interval
    const std::string routine_name = "";
    const std::chrono::milliseconds time(days(1) + std::chrono::hours(4));
    const std::chrono::minutes interval(std::chrono::hours(2));

    setupBalancedIntervalTest(time, interval, routine_name);
    expectWaitTimeInDebug(interval);
}

TEST_F(MainloopTest, balanced_interval_empty_routine_name_middle_of_the_hour)
{
    Debug::setUnitTestFlag(D_MAINLOOP, Debug::DebugLevel::TRACE);
    // Start 1 day and 5:54 (in the middle of the second hour)
    // Interval is 2 hours
    // Because routine_name is empty, no shifting will be done so remaining time will be 6 minutes
    const std::string routine_name = "";
    const std::chrono::milliseconds time(days(1) + std::chrono::hours(5) + std::chrono::minutes(54));
    const std::chrono::minutes interval(std::chrono::hours(2));

    setupBalancedIntervalTest(time, interval, routine_name);
    expectWaitTimeInDebug(std::chrono::minutes(6));
}

TEST_F(MainloopTest, balanced_interval_non_empty_routine_name_hour_start)
{
    Debug::setUnitTestFlag(D_MAINLOOP, Debug::DebugLevel::TRACE);
    // Start on day 1 at 4:00 (at the start of the hour)
    // Interval is 2 hours
    // The routine_name is chosen so that it results in hashed slot #2 which shifts the remaining time by 2 * 10
    // minutes So, the remaining time to wait would be original interval + 20 minutes
    const std::string routine_name = "5e9dac5d204a8f35b264a932";
    const std::chrono::milliseconds time(days(1) + std::chrono::hours(4));
    const std::chrono::minutes interval(std::chrono::hours(2));
    const std::chrono::microseconds offset = std::chrono::minutes(2*10);

    setupBalancedIntervalTest(time, interval, routine_name, offset);

    expectWaitTimeInDebug(offset);
}

TEST_F(MainloopTest, balanced_interval_non_empty_routine_name_hour_start_post_offset)
{
    Debug::setUnitTestFlag(D_MAINLOOP, Debug::DebugLevel::TRACE);
    // Start on day 1 at 4:40 (at the start of the hour)
    // Interval is 2 hours
    // The routine_name is chosen so that it results in hashed slot #2 which shifts the remaining time by 2 * 10
    // minutes So, the remaining time to wait would be original interval - 20 minutes
    const std::string routine_name = "5e9dac5d204a8f35b264a932";
    const std::chrono::milliseconds time(days(1) + std::chrono::hours(4) + std::chrono::minutes(40));
    const std::chrono::minutes interval(std::chrono::hours(2));
    const std::chrono::microseconds offset = std::chrono::minutes(2*10);

    setupBalancedIntervalTest(time, interval, routine_name, offset);

    expectWaitTimeInDebug(interval - offset);
}

TEST_F(MainloopTest, balanced_interval_non_empty_routine_name_middle_of_the_hour)
{
    Debug::setUnitTestFlag(D_MAINLOOP, Debug::DebugLevel::TRACE);
    // Start 1 day and 5:54 (in the middle of the second hour)
    // Interval is 2 hours
    // The routine_name is chosen so that it results in hashed slot #2 which shifts the remaining time by 2 * 10
    // minutes So, the remaining time to wait would be 6 minutes + 20 minutes shift
    const std::string routine_name = "5e9dac5d204a8f35b264a932";
    const std::chrono::milliseconds time(days(1) + std::chrono::hours(5) + std::chrono::minutes(54));
    const std::chrono::minutes interval(std::chrono::hours(2));
    const std::chrono::microseconds offset = std::chrono::minutes(2*10);

    setupBalancedIntervalTest(time, interval, routine_name, offset);

    expectWaitTimeInDebug(std::chrono::minutes(6) + offset);
}

TEST_F(MainloopTest, balanced_interval_another_asset_hour_start)
{
    Debug::setUnitTestFlag(D_MAINLOOP, Debug::DebugLevel::TRACE);
    // Start on day 1 at 4:00 (at the start of the hour)
    // Interval is 2 hours
    // The routine_name is chosen so that it results in hashed slot #7 which shifts the remaining time by 7 * 10
    // minutes So, the remaining time to wait would be 70 minutes
    const std::string routine_name = "5e9da89572f6f9af9bebc0da";
    const std::chrono::milliseconds time(days(1) + std::chrono::hours(4));
    const std::chrono::minutes interval(std::chrono::hours(2));
    const std::chrono::microseconds offset = std::chrono::minutes(7*10);

    setupBalancedIntervalTest(time, interval, routine_name, offset);

    expectWaitTimeInDebug(offset);
}
