// This file Copyright (C) 2022 Mnemosyne LLC.
// It may be used under GPLv2 (SPDX: GPL-2.0-only), GPLv3 (SPDX: GPL-3.0-only),
// or any future license endorsed by Mnemosyne LLC.
// License text can be found in the licenses/ folder.

#include <chrono>
#include <cstddef> // size_t

#include <event2/event.h>

#include <gtest/gtest.h>

#include <libtransmission/timer.h>
#include <libtransmission/timer-ev.h>
#include <libtransmission/utils-ev.h>

#include "test-fixtures.h"

namespace tr::test
{
class TimerTest : public TransmissionTest
{
protected:
    // setup + teardown to manage an event_base

    void SetUp() override
    {
        ::testing::Test::SetUp();
        evbase_.reset(event_base_new());
    }

    void TearDown() override
    {
        evbase_.reset();
        ::testing::Test::TearDown();
    }

    static auto constexpr AsMSec = [](auto val)
    {
        return std::chrono::duration_cast<std::chrono::milliseconds>(val);
    };

    void sleepMsec(std::chrono::milliseconds msec)
    {
        EXPECT_FALSE(waitFor(evbase_.get(), []() { return false; }, msec));
    }

    static void expectTime(
        std::chrono::milliseconds expected,
        std::chrono::milliseconds actual,
        std::chrono::milliseconds allowed_deviation)
    {
        auto const lower_bound = expected - allowed_deviation;
        EXPECT_LT(lower_bound, actual) << "lower_bound:" << lower_bound.count() << " actual:" << actual.count();

        auto const upper_bound = expected + allowed_deviation;
        EXPECT_LT(actual, upper_bound) << "actual:" << actual.count() << " upper_bound:" << upper_bound.count();
    }

    // This checks that `actual` is in the bounds of [expected/2 ... expected*1.5]
    // to confirm that the timer didn't kick too close to the previous or next interval.
    static void expectInterval(std::chrono::milliseconds expected, std::chrono::milliseconds actual)
    {
        expectTime(expected, actual, expected / 2);
    }

    [[nodiscard]] static auto currentTime()
    {
        return std::chrono::steady_clock::now();
    }

    evhelpers::evbase_unique_ptr evbase_;
};

TEST_F(TimerTest, canInstantiate)
{
    auto timer_maker = EvTimerMaker{ evbase_.get() };
    auto timer = timer_maker.create();
    EXPECT_TRUE(timer);
}

TEST_F(TimerTest, singleShotCallsCallback)
{
    auto timer_maker = EvTimerMaker{ evbase_.get() };
    auto timer = timer_maker.create();
    EXPECT_TRUE(timer);

    auto called = false;
    auto callback = [&called]()
    {
        called = true;
    };
    timer->set_callback(callback);

    static auto constexpr Interval = 100ms;
    timer->start_single_shot(Interval);

    waitFor(evbase_.get(), [&called] { return called; });
    EXPECT_TRUE(called);
}

TEST_F(TimerTest, repeatingCallsCallback)
{
    auto timer_maker = EvTimerMaker{ evbase_.get() };
    auto timer = timer_maker.create();
    EXPECT_TRUE(timer);

    auto called = false;
    auto callback = [&called]()
    {
        called = true;
    };
    timer->set_callback(callback);

    static auto constexpr Interval = 100ms;
    timer->start_repeating(Interval);

    waitFor(evbase_.get(), [&called] { return called; });
    EXPECT_TRUE(called);
}

TEST_F(TimerTest, singleShotHonorsInterval)
{
    auto timer_maker = EvTimerMaker{ evbase_.get() };
    auto timer = timer_maker.create();
    EXPECT_TRUE(timer);

    auto called = false;
    auto callback = [&called]()
    {
        called = true;
    };
    timer->set_callback(callback);

    // run a single-shot timer
    auto const begin_time = currentTime();
    static auto constexpr Interval = 100ms;
    timer->start_single_shot(Interval);
    EXPECT_FALSE(timer->is_repeating());
    EXPECT_EQ(Interval, timer->interval());
    waitFor(evbase_.get(), [&called] { return called; });
    auto const end_time = currentTime();

    // confirm that it kicked at the right interval
    EXPECT_TRUE(called);
    expectInterval(Interval, AsMSec(end_time - begin_time));
}

TEST_F(TimerTest, repeatingHonorsInterval)
{
    auto timer_maker = EvTimerMaker{ evbase_.get() };
    auto timer = timer_maker.create();
    EXPECT_TRUE(timer);

    auto n_calls = size_t{ 0U };
    auto callback = [&n_calls]()
    {
        ++n_calls;
    };
    timer->set_callback(callback);

    // start a repeating timer
    auto const begin_time = currentTime();
    static auto constexpr Interval = 100ms;
    static auto constexpr DesiredLoops = 3;
    timer->start_repeating(Interval);
    EXPECT_TRUE(timer->is_repeating());
    EXPECT_EQ(Interval, timer->interval());
    waitFor(evbase_.get(), [&n_calls] { return n_calls >= DesiredLoops; });
    auto const end_time = currentTime();

    // confirm that it kicked the right number of times
    expectInterval(Interval * DesiredLoops, AsMSec(end_time - begin_time));
    EXPECT_EQ(DesiredLoops, n_calls);
}

// TODO(ckerr): flaky test should be fixed instead of disabled
TEST_F(TimerTest, DISABLED_restartWithDifferentInterval)
{
    auto timer_maker = EvTimerMaker{ evbase_.get() };
    auto timer = timer_maker.create();
    EXPECT_TRUE(timer);

    auto n_calls = size_t{ 0U };
    auto callback = [&n_calls]()
    {
        ++n_calls;
    };
    timer->set_callback(callback);

    auto const test = [this, &n_calls, &timer](auto interval)
    {
        auto const next = n_calls + 1;
        auto const begin_time = currentTime();
        timer->start_single_shot(interval);
        waitFor(evbase_.get(), [&n_calls, next]() { return n_calls >= next; });
        auto const end_time = currentTime();

        expectInterval(interval, AsMSec(end_time - begin_time));
    };

    test(200ms);
    test(400ms);
    test(200ms);
}

// TODO(ckerr): flaky test should be fixed instead of disabled
TEST_F(TimerTest, DISABLED_restartWithSameInterval)
{
    auto timer_maker = EvTimerMaker{ evbase_.get() };
    auto timer = timer_maker.create();
    EXPECT_TRUE(timer);

    auto n_calls = size_t{ 0U };
    auto callback = [&n_calls]()
    {
        ++n_calls;
    };
    timer->set_callback(callback);

    auto const test = [this, &n_calls, &timer](auto interval)
    {
        auto const next = n_calls + 1;
        auto const begin_time = currentTime();
        timer->start_single_shot(interval);
        waitFor(evbase_.get(), [&n_calls, next]() { return n_calls >= next; });
        auto const end_time = currentTime();

        expectInterval(interval, AsMSec(end_time - begin_time));
    };

    test(timer->interval());
    test(timer->interval());
    test(timer->interval());
}

// TODO(ckerr): flaky test should be fixed instead of disabled
TEST_F(TimerTest, DISABLED_repeatingThenSingleShot)
{
    auto timer_maker = EvTimerMaker{ evbase_.get() };
    auto timer = timer_maker.create();
    EXPECT_TRUE(timer);

    auto n_calls = size_t{ 0U };
    auto callback = [&n_calls]()
    {
        ++n_calls;
    };
    timer->set_callback(callback);

    // start a repeating timer and confirm that it's running
    auto begin_time = currentTime();
    static auto constexpr RepeatingInterval = 100ms;
    static auto constexpr DesiredLoops = 2;
    timer->start_repeating(RepeatingInterval);
    EXPECT_EQ(RepeatingInterval, timer->interval());
    EXPECT_TRUE(timer->is_repeating());
    waitFor(evbase_.get(), [&n_calls]() { return n_calls >= DesiredLoops; });
    auto end_time = currentTime();
    expectTime(RepeatingInterval * DesiredLoops, AsMSec(end_time - begin_time), RepeatingInterval / 2);

    // now restart it as a single shot
    auto const baseline = n_calls;
    begin_time = currentTime();
    static auto constexpr SingleShotInterval = 200ms;
    timer->start_single_shot(SingleShotInterval);
    EXPECT_EQ(SingleShotInterval, timer->interval());
    EXPECT_FALSE(timer->is_repeating());
    waitFor(evbase_.get(), [&n_calls]() { return n_calls >= DesiredLoops + 1; });
    end_time = currentTime();

    // confirm that the single shot interval was honored
    expectInterval(SingleShotInterval, AsMSec(end_time - begin_time));

    // confirm that the timer only kicks once, since it was converted into single-shot
    sleepMsec(SingleShotInterval * 3);
    EXPECT_EQ(baseline + 1, n_calls);
}

// TODO(ckerr): flaky test should be fixed instead of disabled
TEST_F(TimerTest, DISABLED_singleShotStop)
{
    auto timer_maker = EvTimerMaker{ evbase_.get() };
    auto timer = timer_maker.create();
    EXPECT_TRUE(timer);

    auto n_calls = size_t{ 0U };
    auto callback = [&n_calls]()
    {
        ++n_calls;
    };
    timer->set_callback(callback);

    // start a single-shot timer
    static auto constexpr Interval = 200ms;
    timer->start_single_shot(Interval);
    EXPECT_EQ(Interval, timer->interval());
    EXPECT_FALSE(timer->is_repeating());

    // wait half the interval, then stop the timer
    sleepMsec(Interval / 2);
    EXPECT_EQ(0U, n_calls);
    timer->stop();

    // wait until the timer has gone past.
    // since we stopped it, callback should not have been called.
    sleepMsec(Interval);
    EXPECT_EQ(0U, n_calls);
}

TEST_F(TimerTest, repeatingStop)
{
    auto timer_maker = EvTimerMaker{ evbase_.get() };
    auto timer = timer_maker.create();
    EXPECT_TRUE(timer);

    auto n_calls = size_t{ 0U };
    auto callback = [&n_calls]()
    {
        ++n_calls;
    };
    timer->set_callback(callback);

    // start a repeating timer
    static auto constexpr Interval = 200ms;
    timer->start_repeating(Interval);
    EXPECT_EQ(Interval, timer->interval());
    EXPECT_TRUE(timer->is_repeating());

    // wait half the interval, then stop the timer
    sleepMsec(Interval / 2);
    EXPECT_EQ(0U, n_calls);
    timer->stop();

    // wait until the timer has gone past.
    // since we stopped it, callback should not have been called.
    sleepMsec(Interval);
    EXPECT_EQ(0U, n_calls);
}

TEST_F(TimerTest, destroyedTimersStop)
{
    auto timer_maker = EvTimerMaker{ evbase_.get() };
    auto timer = timer_maker.create();
    EXPECT_TRUE(timer);

    auto n_calls = size_t{ 0U };
    auto callback = [&n_calls]()
    {
        ++n_calls;
    };
    timer->set_callback(callback);

    // start a repeating timer
    static auto constexpr Interval = 200ms;
    timer->start_repeating(Interval);
    EXPECT_EQ(Interval, timer->interval());
    EXPECT_TRUE(timer->is_repeating());

    // wait half the interval, then destroy the timer
    sleepMsec(Interval / 2);
    EXPECT_EQ(0U, n_calls);
    timer.reset();

    // wait until the timer has gone past.
    // since we destroyed it, callback should not have been called.
    sleepMsec(Interval);
    EXPECT_EQ(0U, n_calls);
}

} // namespace tr::test
