/*
 * Copyright (C) 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <chrono>
#include <thread>
#include <gtest/gtest.h>
#include <mediautils/TimerThread.h>

using namespace std::chrono_literals;
using namespace android::mediautils;

namespace {

constexpr auto kJitter = 10ms;

// Each task written by *ToString() will start with a left brace.
constexpr char REQUEST_START = '{';

inline size_t countChars(std::string_view s, char c) {
    return std::count(s.begin(), s.end(), c);
}


// Split msec time between timeout and second chance time
// This tests expiration times weighted between timeout and the second chance time.
#define DISTRIBUTE_TIMEOUT_SECONDCHANCE_MS_FRAC(msec, frac) \
    std::chrono::milliseconds(int((msec) * (frac)) + 1), \
    std::chrono::milliseconds(int((msec) * (1.f - (frac))))

// The TimerThreadTest is parameterized on a fraction between 0.f and 1.f which
// is how the total timeout time is split between the first timeout and the second chance time.
//
class TimerThreadTest : public ::testing::TestWithParam<float> {
protected:

static void testBasic() {
    const auto frac = GetParam();

    std::atomic<bool> taskRan = false;
    TimerThread thread;
    TimerThread::Handle handle =
            thread.scheduleTask("Basic", [&taskRan](TimerThread::Handle) {
                    taskRan = true; }, DISTRIBUTE_TIMEOUT_SECONDCHANCE_MS_FRAC(100, frac));
    ASSERT_TRUE(TimerThread::isTimeoutHandle(handle));
    std::this_thread::sleep_for(100ms - kJitter);
    ASSERT_FALSE(taskRan);
    std::this_thread::sleep_for(2 * kJitter);
    ASSERT_TRUE(taskRan); // timed-out called.
    ASSERT_EQ(1ul, countChars(thread.timeoutToString(), REQUEST_START));
    // nothing cancelled
    ASSERT_EQ(0ul, countChars(thread.retiredToString(), REQUEST_START));
}

static void testCancel() {
    const auto frac = GetParam();

    std::atomic<bool> taskRan = false;
    TimerThread thread;
    TimerThread::Handle handle =
            thread.scheduleTask("Cancel", [&taskRan](TimerThread::Handle) {
                    taskRan = true; }, DISTRIBUTE_TIMEOUT_SECONDCHANCE_MS_FRAC(100, frac));
    ASSERT_TRUE(TimerThread::isTimeoutHandle(handle));
    std::this_thread::sleep_for(100ms - kJitter);
    ASSERT_FALSE(taskRan);
    ASSERT_TRUE(thread.cancelTask(handle));
    std::this_thread::sleep_for(2 * kJitter);
    ASSERT_FALSE(taskRan); // timed-out did not call.
    ASSERT_EQ(0ul, countChars(thread.timeoutToString(), REQUEST_START));
    // task cancelled.
    ASSERT_EQ(1ul, countChars(thread.retiredToString(), REQUEST_START));
}

static void testCancelAfterRun() {
    const auto frac = GetParam();

    std::atomic<bool> taskRan = false;
    TimerThread thread;
    TimerThread::Handle handle =
            thread.scheduleTask("CancelAfterRun",
                    [&taskRan](TimerThread::Handle) {
                            taskRan = true; },
                            DISTRIBUTE_TIMEOUT_SECONDCHANCE_MS_FRAC(100, frac));
    ASSERT_TRUE(TimerThread::isTimeoutHandle(handle));
    std::this_thread::sleep_for(100ms + kJitter);
    ASSERT_TRUE(taskRan); //  timed-out called.
    ASSERT_FALSE(thread.cancelTask(handle));
    ASSERT_EQ(1ul, countChars(thread.timeoutToString(), REQUEST_START));
    // nothing actually cancelled
    ASSERT_EQ(0ul, countChars(thread.retiredToString(), REQUEST_START));
}

static void testMultipleTasks() {
    const auto frac = GetParam();

    std::array<std::atomic<bool>, 6> taskRan{};
    TimerThread thread;

    auto startTime = std::chrono::steady_clock::now();

    thread.scheduleTask("0", [&taskRan](TimerThread::Handle) {
            taskRan[0] = true; }, DISTRIBUTE_TIMEOUT_SECONDCHANCE_MS_FRAC(300, frac));
    thread.scheduleTask("1", [&taskRan](TimerThread::Handle) {
            taskRan[1] = true; }, DISTRIBUTE_TIMEOUT_SECONDCHANCE_MS_FRAC(100, frac));
    thread.scheduleTask("2", [&taskRan](TimerThread::Handle) {
            taskRan[2] = true; }, DISTRIBUTE_TIMEOUT_SECONDCHANCE_MS_FRAC(200, frac));
    thread.scheduleTask("3", [&taskRan](TimerThread::Handle) {
            taskRan[3] = true; }, DISTRIBUTE_TIMEOUT_SECONDCHANCE_MS_FRAC(400, frac));
    auto handle4 = thread.scheduleTask("4", [&taskRan](TimerThread::Handle) {
            taskRan[4] = true; }, DISTRIBUTE_TIMEOUT_SECONDCHANCE_MS_FRAC(200, frac));
    thread.scheduleTask("5", [&taskRan](TimerThread::Handle) {
            taskRan[5] = true; }, DISTRIBUTE_TIMEOUT_SECONDCHANCE_MS_FRAC(200, frac));

    // 6 tasks pending
    ASSERT_EQ(6ul, countChars(thread.pendingToString(), REQUEST_START));
    // 0 tasks completed
    ASSERT_EQ(0ul, countChars(thread.retiredToString(), REQUEST_START));

    // None of the tasks are expected to have finished at the start.
    std::array<std::atomic<bool>, 6> expected{};

    // Task 1 should trigger around 100ms.
    std::this_thread::sleep_until(startTime + 100ms - kJitter);

    ASSERT_EQ(expected, taskRan);


    std::this_thread::sleep_until(startTime + 100ms + kJitter);

    expected[1] = true;
    ASSERT_EQ(expected, taskRan);

    // Cancel task 4 before it gets a chance to run.
    thread.cancelTask(handle4);

    // Tasks 2 and 5 should trigger around 200ms.
    std::this_thread::sleep_until(startTime + 200ms - kJitter);

    ASSERT_EQ(expected, taskRan);


    std::this_thread::sleep_until(startTime + 200ms + kJitter);

    expected[2] = true;
    expected[5] = true;
    ASSERT_EQ(expected, taskRan);

    // Task 0 should trigger around 300ms.
    std::this_thread::sleep_until(startTime + 300ms - kJitter);

    ASSERT_EQ(expected, taskRan);

    std::this_thread::sleep_until(startTime + 300ms + kJitter);

    expected[0] = true;
    ASSERT_EQ(expected, taskRan);

    // 1 task pending
    ASSERT_EQ(1ul, countChars(thread.pendingToString(), REQUEST_START));
    // 4 tasks called on timeout,  and 1 cancelled
    ASSERT_EQ(4ul, countChars(thread.timeoutToString(), REQUEST_START));
    ASSERT_EQ(1ul, countChars(thread.retiredToString(), REQUEST_START));

    // Task 3 should trigger around 400ms.
    std::this_thread::sleep_until(startTime + 400ms - kJitter);

    ASSERT_EQ(expected, taskRan);

    // 4 tasks called on timeout and 1 cancelled
    ASSERT_EQ(4ul, countChars(thread.timeoutToString(), REQUEST_START));
    ASSERT_EQ(1ul, countChars(thread.retiredToString(), REQUEST_START));

    std::this_thread::sleep_until(startTime + 400ms + kJitter);

    expected[3] = true;
    ASSERT_EQ(expected, taskRan);

    // 0 tasks pending
    ASSERT_EQ(0ul, countChars(thread.pendingToString(), REQUEST_START));
    // 5 tasks called on timeout and 1 cancelled
    ASSERT_EQ(5ul, countChars(thread.timeoutToString(), REQUEST_START));
    ASSERT_EQ(1ul, countChars(thread.retiredToString(), REQUEST_START));
}

}; // class TimerThreadTest

TEST_P(TimerThreadTest, Basic) {
    testBasic();
}

TEST_P(TimerThreadTest, Cancel) {
    testCancel();
}

TEST_P(TimerThreadTest, CancelAfterRun) {
    testCancelAfterRun();
}

TEST_P(TimerThreadTest, MultipleTasks) {
    testMultipleTasks();
}

INSTANTIATE_TEST_CASE_P(
        TimerThread,
        TimerThreadTest,
        ::testing::Values(0.f, 0.5f, 1.f)
        );

TEST(TimerThread, TrackedTasks) {
    TimerThread thread;

    auto handle0 = thread.trackTask("0");
    auto handle1 = thread.trackTask("1");
    auto handle2 = thread.trackTask("2");

    ASSERT_TRUE(TimerThread::isNoTimeoutHandle(handle0));
    ASSERT_TRUE(TimerThread::isNoTimeoutHandle(handle1));
    ASSERT_TRUE(TimerThread::isNoTimeoutHandle(handle2));

    // 3 tasks pending
    ASSERT_EQ(3ul, countChars(thread.pendingToString(), REQUEST_START));
    // 0 tasks retired
    ASSERT_EQ(0ul, countChars(thread.retiredToString(), REQUEST_START));

    ASSERT_TRUE(thread.cancelTask(handle0));
    ASSERT_TRUE(thread.cancelTask(handle1));

    // 1 task pending
    ASSERT_EQ(1ul, countChars(thread.pendingToString(), REQUEST_START));
    // 2 tasks retired
    ASSERT_EQ(2ul, countChars(thread.retiredToString(), REQUEST_START));

    // handle1 is stale, cancel returns false.
    ASSERT_FALSE(thread.cancelTask(handle1));

    // 1 task pending
    ASSERT_EQ(1ul, countChars(thread.pendingToString(), REQUEST_START));
    // 2 tasks retired
    ASSERT_EQ(2ul, countChars(thread.retiredToString(), REQUEST_START));

    // Add another tracked task.
    auto handle3 = thread.trackTask("3");
    ASSERT_TRUE(TimerThread::isNoTimeoutHandle(handle3));

    // 2 tasks pending
    ASSERT_EQ(2ul, countChars(thread.pendingToString(), REQUEST_START));
    // 2 tasks retired
    ASSERT_EQ(2ul, countChars(thread.retiredToString(), REQUEST_START));

    ASSERT_TRUE(thread.cancelTask(handle2));

    // 1 tasks pending
    ASSERT_EQ(1ul, countChars(thread.pendingToString(), REQUEST_START));
    // 3 tasks retired
    ASSERT_EQ(3ul, countChars(thread.retiredToString(), REQUEST_START));

    ASSERT_TRUE(thread.cancelTask(handle3));

    // 0 tasks pending
    ASSERT_EQ(0ul, countChars(thread.pendingToString(), REQUEST_START));
    // 4 tasks retired
    ASSERT_EQ(4ul, countChars(thread.retiredToString(), REQUEST_START));
}

}  // namespace
