#define Uses_TTimerQueue
#include <tvision/tv.h>

#include <test.h>

static TTimePoint currentTime;
static int currentTimeRequests;
static TTimePoint mockCurrentTime()
{
    ++currentTimeRequests;
    return currentTime;
}

static int timeouts;
static void handleTimeout(TTimerId, void *)
{
    ++timeouts;
}

TEST(TTimerQueue, EmptyQueueShouldReturnNoExpiredTimers)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    int32_t nextTimeout = timerQueue.timeUntilNextTimeout();
    EXPECT_EQ(nextTimeout, -1);

    timeouts = 0;
    timerQueue.collectExpiredTimers(handleTimeout, nullptr);
    EXPECT_EQ(timeouts, 0);
}

TEST(TTimerQueue, ShouldCollectExpiredTimers)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    timerQueue.setTimer(1000);

    currentTime = 1000;
    timeouts = 0;
    timerQueue.collectExpiredTimers(handleTimeout, nullptr);
    EXPECT_EQ(timeouts, 1);
}

TEST(TTimerQueue, ShouldMeasureNextTimeoutProperly)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    timerQueue.setTimer(1000);

    currentTime = 1;
    int32_t nextTimeout = timerQueue.timeUntilNextTimeout();
    EXPECT_EQ(nextTimeout, 999);

    currentTime = 1001;
    nextTimeout = timerQueue.timeUntilNextTimeout();
    EXPECT_EQ(nextTimeout, 0);
}

TEST(TTimerQueue, ShouldUnqueueSingleShotTimer)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    timerQueue.setTimer(1000);

    currentTime = 1000;
    timeouts = 0;
    timerQueue.collectExpiredTimers(handleTimeout, nullptr);

    int32_t nextTimeout = timerQueue.timeUntilNextTimeout();
    EXPECT_EQ(nextTimeout, -1);
}

TEST(TTimerQueue, ShouldRequeuePeriodicTimer)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    timerQueue.setTimer(1000, 500);

    int32_t nextTimeout = timerQueue.timeUntilNextTimeout();
    EXPECT_EQ(nextTimeout, 1000);

    currentTime = 1000;
    timeouts = 0;
    timerQueue.collectExpiredTimers(handleTimeout, nullptr);
    EXPECT_EQ(timeouts, 1);

    timeouts = 0;
    timerQueue.collectExpiredTimers(handleTimeout, nullptr);
    EXPECT_EQ(timeouts, 0);

    nextTimeout = timerQueue.timeUntilNextTimeout();
    EXPECT_EQ(nextTimeout, 500);

    currentTime = 1500;
    timeouts = 0;
    timerQueue.collectExpiredTimers(handleTimeout, nullptr);
    EXPECT_EQ(timeouts, 1);
}

TEST(TTimerQueue, ShouldCollectOnlyExpiredTimeouts)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    timerQueue.setTimer(1500);
    timerQueue.setTimer(1000);

    currentTime = 1000;
    timeouts = 0;
    timerQueue.collectExpiredTimers(handleTimeout, nullptr);
    EXPECT_EQ(timeouts, 1);

    int32_t nextTimeout = timerQueue.timeUntilNextTimeout();
    EXPECT_EQ(nextTimeout, 500);
}

TEST(TTimerQueue, ShouldMeasureNextTimeoutProperlyWithSeveralTimers)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    timerQueue.setTimer(1500);
    timerQueue.setTimer(1000);

    currentTime = 1;
    int32_t nextTimeout = timerQueue.timeUntilNextTimeout();
    EXPECT_EQ(nextTimeout, 999);
}

TEST(TTimerQueue, ShouldCollectSeveralExpiredTimers)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    timerQueue.setTimer(1500);
    timerQueue.setTimer(1000);

    currentTime = 1500;
    timeouts = 0;
    timerQueue.collectExpiredTimers(handleTimeout, nullptr);
    EXPECT_EQ(timeouts, 2);
}

TEST(TTimerQueue, ShouldRemoveTimer)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    timerQueue.setTimer(1500);
    TTimerId id2 = timerQueue.setTimer(1000);

    timerQueue.killTimer(id2);

    currentTime = 1;
    int32_t nextTimeout = timerQueue.timeUntilNextTimeout();
    EXPECT_EQ(nextTimeout, 1499);
}

TEST(TTimerQueue, RemovingInvalidTimersShouldNotProduceErrors)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    timerQueue.setTimer(1500);
    TTimerId id2 = timerQueue.setTimer(1000);

    timerQueue.killTimer(id2);
    timerQueue.killTimer(id2);
    timerQueue.killTimer(nullptr);
    timerQueue.killTimer((TTimerId) 3);

    currentTime = 1;
    int32_t nextTimeout = timerQueue.timeUntilNextTimeout();
    EXPECT_EQ(nextTimeout, 1499);
}

TEST(TTimerQueue, ShouldHandleZeroTimedTimersProperly)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    timerQueue.setTimer(0, 0);
    timerQueue.setTimer(0, 0);
    timerQueue.setTimer(0, 0);

    timeouts = 0;
    timerQueue.collectExpiredTimers(handleTimeout, nullptr);
    EXPECT_EQ(timeouts, 3);

    timeouts = 0;
    timerQueue.collectExpiredTimers(handleTimeout, nullptr);
    EXPECT_EQ(timeouts, 3);
}

static void nestedHandleTimeout(TTimerId, void *args)
{
    ++timeouts;
    currentTime = 1500;
    (*(TTimerQueue *) args).collectExpiredTimers(handleTimeout, nullptr);
}

TEST(TTimerQueue, ShouldNotCollectTimeoutsWithNestedInvocation)
{
    currentTime = 0;
    TTimerQueue timerQueue(mockCurrentTime);

    timerQueue.setTimer(0, 0);
    timerQueue.setTimer(1000, 500);

    currentTime = 1000;
    timeouts = 0;
    timerQueue.collectExpiredTimers(nestedHandleTimeout, &timerQueue);
    EXPECT_EQ(timeouts, 2);
}

TEST(TTimerQueue, ShouldNotRequestCurrentTimeIfThereAreNoTimers)
{
    currentTimeRequests = 0;

    TTimerQueue timerQueue(mockCurrentTime);
    timerQueue.collectExpiredTimers(handleTimeout, nullptr);

    EXPECT_EQ(currentTimeRequests, 0);
}
