/*
 * Copyright (c) 2018 ARM Limited
 * All rights reserved
 *
 * The license below extends only to copyright in the software and shall
 * not be construed as granting a license to any other intellectual
 * property including but not limited to intellectual property relating
 * to a hardware implementation of the functionality of the software
 * licensed hereunder.  You may use the software subject to the license
 * terms below provided that you ensure that this notice is replicated
 * unmodified and in its entirety in all distributions of the software,
 * modified or unmodified, in source code or in binary form.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met: redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer;
 * redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution;
 * neither the name of the copyright holders nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Authors: Giacomo Travaglini
 */

#include <gtest/gtest.h>

#include "base/coroutine.hh"

using namespace m5;

/**
 * This test is checking if the Coroutine, once it yields
 * back to the caller, it is still marked as not finished.
 */
TEST(Coroutine, Unfinished)
{
    auto yielding_task =
    [] (Coroutine<void, void>::CallerType& yield)
    {
        yield();
    };

    Coroutine<void, void> coro(yielding_task);
    ASSERT_TRUE(coro);
}

/**
 * This test is checking the parameter passing interface of a
 * coroutine which takes an integer as an argument.
 * Coroutine::operator() and CallerType::get() are the tested
 * APIS.
 */
TEST(Coroutine, Passing)
{
    const std::vector<int> input{ 1, 2, 3 };
    const std::vector<int> expected_values = input;

    auto passing_task =
    [&expected_values] (Coroutine<int, void>::CallerType& yield)
    {
        int argument;

        for (const auto expected : expected_values) {
            argument = yield.get();
            ASSERT_EQ(argument, expected);
        }
    };

    Coroutine<int, void> coro(passing_task);
    ASSERT_TRUE(coro);

    for (const auto val : input) {
        coro(val);
    }
}

/**
 * This test is checking the yielding interface of a coroutine
 * which takes no argument and returns integers.
 * Coroutine::get() and CallerType::operator() are the tested
 * APIS.
 */
TEST(Coroutine, Returning)
{
    const std::vector<int> output{ 1, 2, 3 };
    const std::vector<int> expected_values = output;

    auto returning_task =
    [&output] (Coroutine<void, int>::CallerType& yield)
    {
        for (const auto ret : output) {
            yield(ret);
        }
    };

    Coroutine<void, int> coro(returning_task);
    ASSERT_TRUE(coro);

    for (const auto expected : expected_values) {
        int returned = coro.get();
        ASSERT_EQ(returned, expected);
    }
}

/**
 * This test is still supposed to test the returning interface
 * of the the Coroutine, proving how coroutine can be used
 * for generators.
 * The coroutine is computing the first #steps of the fibonacci
 * sequence and it is yielding back results one number per time.
 */
TEST(Coroutine, Fibonacci)
{
    const std::vector<int> expected_values{
        1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233 };

    const int steps = expected_values.size();

    auto fibonacci_task =
    [steps] (Coroutine<void, int>::CallerType& yield)
    {
        int prev = 0;
        int current = 1;

        for (auto iter = 0; iter < steps; iter++) {
            int sum = prev + current;
            yield(sum);

            prev = current;
            current = sum;
        }
    };

    Coroutine<void, int> coro(fibonacci_task);
    ASSERT_TRUE(coro);

    for (const auto expected : expected_values) {
        ASSERT_TRUE(coro);
        int returned = coro.get();
        ASSERT_EQ(returned, expected);
    }
}

/**
 * This test is using a bi-channel coroutine (accepting and
 * yielding values) for testing a cooperative task.
 * The caller and the coroutine have a string each; they are
 * composing a new string by merging the strings together one
 * character per time.
 * The result string is hence passed back and forth between the
 * coroutine and the caller.
 */
TEST(Coroutine, Cooperative)
{
    const std::string caller_str("HloWrd");
    const std::string coro_str("el ol!");
    const std::string expected("Hello World!");

    auto cooperative_task =
    [&coro_str] (Coroutine<std::string, std::string>::CallerType& yield)
    {
        for (auto& appended_c : coro_str) {
            auto old_str = yield.get();
            yield(old_str + appended_c);
        }
    };

    Coroutine<std::string, std::string> coro(cooperative_task);

    std::string result;
    for (auto& c : caller_str) {
        ASSERT_TRUE(coro);
        result += c;
        result = coro(result).get();
    }

    ASSERT_EQ(result, expected);
}

/**
 * This test is testing nested coroutines by using one inner and one
 * outer coroutine. It basically ensures that yielding from the inner
 * coroutine returns to the outer coroutine (mid-layer of execution) and
 * not to the outer caller.
 */
TEST(Coroutine, Nested)
{
    const std::string wrong("Inner");
    const std::string expected("Inner + Outer");

    auto inner_task =
    [] (Coroutine<void, std::string>::CallerType& yield)
    {
        std::string inner_string("Inner");
        yield(inner_string);
    };

    auto outer_task =
    [&inner_task] (Coroutine<void, std::string>::CallerType& yield)
    {
        Coroutine<void, std::string> coro(inner_task);
        std::string inner_string = coro.get();

        std::string outer_string("Outer");
        yield(inner_string + " + " + outer_string);
    };


    Coroutine<void, std::string> coro(outer_task);
    ASSERT_TRUE(coro);

    std::string result = coro.get();

    ASSERT_NE(result, wrong);
    ASSERT_EQ(result, expected);
}

/**
 * This test is stressing the scenario where two distinct fibers are
 * calling the same coroutine.  First the test instantiates (and runs) a
 * coroutine, then spawns another one and it passes it a reference to
 * the first coroutine. Once the new coroutine calls the first coroutine
 * and the first coroutine yields, we are expecting execution flow to
 * be yielded to the second caller (the second coroutine) and not the
 * original caller (the test itself)
 */
TEST(Coroutine, TwoCallers)
{
    bool valid_return = false;

    Coroutine<void, void> callee{[]
        (Coroutine<void, void>::CallerType& yield)
    {
        yield();
        yield();
    }};

    Coroutine<void, void> other_caller{[&callee, &valid_return]
        (Coroutine<void, void>::CallerType& yield)
    {
        callee();
        valid_return = true;
        yield();
    }};

    ASSERT_TRUE(valid_return);
}