/*
 * 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/circular_queue.hh"

/** Testing that once instantiated with a fixed size,
 * the queue is still empty */
TEST(CircularQueueTest, Empty)
{
    const auto cq_size = 8;
    CircularQueue<uint32_t> cq(cq_size);

    ASSERT_EQ(cq.capacity(), cq_size);
    ASSERT_EQ(cq.size(), 0);
    ASSERT_TRUE(cq.empty());
}

/** Testing that once instantiated with a fixed size,
 * the queue has Head = Tail + 1 */
TEST(CircularQueueTest, HeadTailEmpty)
{
    const auto cq_size = 8;
    CircularQueue<uint32_t> cq(cq_size);
    ASSERT_EQ(cq.head(), cq.tail() + 1);
}

/** Adding elements to the circular queue.
 * Once an element has been added we test the new value
 * of front() and back() (head an tail). Since we are just
 * adding elements and not removing them, we expect the front
 * value to be fixed and the back value to change, matching
 * the latest pushed value.*/
TEST(CircularQueueTest, AddingElements)
{
    const auto cq_size = 8;
    CircularQueue<uint32_t> cq(cq_size);

    const auto first_element = 0xAAAAAAAA;
    cq.push_back(first_element);
    ASSERT_EQ(cq.front(), first_element);
    ASSERT_EQ(cq.back(), first_element);

    const auto second_element = 0x55555555;
    cq.push_back(second_element);
    ASSERT_EQ(cq.front(), first_element);
    ASSERT_EQ(cq.back(), second_element);

    ASSERT_EQ(cq.size(), 2);
}

/** Removing elements from the circular queue.
 * We add two elements and we consequently remove them.
 * After removing them we check that the elements have been
 * effectively removed, which means the circular queue is
 * empty */
TEST(CircularQueueTest, RemovingElements)
{
    const auto cq_size = 8;
    CircularQueue<uint32_t> cq(cq_size);

    // Adding first element
    const auto first_element = 0xAAAAAAAA;
    cq.push_back(first_element);

    // Adding second element
    const auto second_element = 0x55555555;
    cq.push_back(second_element);

    auto initial_head = cq.head();
    auto initial_tail = cq.tail();

    // Removing first and second element
    cq.pop_front();
    ASSERT_EQ(cq.head(), initial_head + 1);
    ASSERT_EQ(cq.tail(), initial_tail);

    cq.pop_front();
    ASSERT_EQ(cq.head(), initial_head + 2);
    ASSERT_EQ(cq.tail(), initial_tail);

    ASSERT_EQ(cq.size(), 0);
    ASSERT_TRUE(cq.empty());
}

/** Testing CircularQueue::full
 * This tests adds elements to the queue and checks that it is full,
 * which means:
 *    - CircularQueue::full == true
 *    - Head = Tail + 1
 */
TEST(CircularQueueTest, Full)
{
    const auto cq_size = 8;
    CircularQueue<uint32_t> cq(cq_size);

    const auto value = 0xAAAAAAAA;
    for (auto idx = 0; idx < cq_size; idx++) {
        cq.push_back(value);
    }

    ASSERT_TRUE(cq.full());
    ASSERT_EQ(cq.head(), cq.tail() + 1);
}

/** Testing CircularQueue::begin(), CircularQueue::end()
 * This tests the following:
 *     - In an empty queue, begin() == end()
 *     - After pushing some elements in the queue, the begin()
 *       and end() iterators are correctly misaligned
 */
TEST(CircularQueueTest, BeginEnd)
{
    const auto cq_size = 8;
    CircularQueue<uint32_t> cq(cq_size);

    // Begin/End are the same (empty)
    ASSERT_EQ(cq.begin(), cq.end());

    const auto first_value = 0xAAAAAAAA;
    const auto second_value = 0x55555555;

    cq.push_back(first_value);
    cq.push_back(second_value);

    // End = Begin + 2
    ASSERT_EQ(cq.begin() + 2, cq.end());
}

/** Testing that begin() and end() (-1) iterators
 * actually point to the correct values
 * so that dereferencing them leads to a match with the
 * values of (front() and back())
 */
TEST(CircularQueueTest, BeginFrontEndBack)
{
    const auto cq_size = 8;
    CircularQueue<uint32_t> cq(cq_size);

    const auto front_value = 0xAAAAAAAA;
    const auto back_value = 0x55555555;

    cq.push_back(front_value);
    cq.push_back(back_value);

    ASSERT_EQ(*(cq.begin()), cq.front());
    ASSERT_EQ(*(cq.end() - 1), cq.back());
}

/** Testing circular queue iterators:
 * By allocating two iterators to a queue we test several
 * operators.
 */
TEST(CircularQueueTest, IteratorsOp)
{
    const auto cq_size = 8;
    CircularQueue<uint32_t> cq(cq_size);

    const auto first_value = 0xAAAAAAAA;
    const auto second_value = 0x55555555;
    cq.push_back(first_value);
    cq.push_back(second_value);

    auto negative_offset = -(cq_size + 1);
    auto it_1 = cq.begin();
    auto it_2 = cq.begin() + 1;
    auto it_3 = cq.begin() - negative_offset;

    // Operators test
    ASSERT_TRUE(it_1 != it_2);
    ASSERT_FALSE(it_1 == it_2);
    ASSERT_FALSE(it_1 > it_2);
    ASSERT_FALSE(it_1 >= it_2);
    ASSERT_TRUE(it_1 < it_2);
    ASSERT_TRUE(it_1 <= it_2);
    ASSERT_EQ(*it_1, first_value);
    ASSERT_EQ(it_1 + 1, it_2);
    ASSERT_EQ(it_1, it_2 - 1);
    ASSERT_EQ(it_2 - it_1, 1);
    ASSERT_EQ(it_1 - it_2, -1);
    ASSERT_EQ(it_3._round, 1);

    auto temp_it = it_1;
    ASSERT_EQ(++temp_it, it_2);
    ASSERT_EQ(--temp_it, it_1);
    ASSERT_EQ(temp_it++, it_1);
    ASSERT_EQ(temp_it, it_2);
    ASSERT_EQ(temp_it--, it_2);
    ASSERT_EQ(temp_it, it_1);
}

/**
 * Testing a full loop, which is incrementing one iterator until
 * it wraps and has the same index as the starting iterator.
 * This test checks that even if they have the same index, they are
 * not the same iterator since they have different round.
 */
TEST(CircularQueueTest, FullLoop)
{
    const auto cq_size = 8;
    CircularQueue<uint32_t> cq(cq_size);

    // ending_it does a full loop and points at the same
    // index as starting_it but with a different round
    auto starting_it = cq.begin();
    auto ending_it = starting_it + cq_size;

    ASSERT_EQ(starting_it._idx, ending_it._idx);
    ASSERT_TRUE(starting_it != ending_it);
}

/**
 * Testing correct behaviour when rounding multiple times:
 * - Round indexes in sync
 * - Difference between begin() and end() iterator is still
 * equal to the CircularQueue size.
 */
TEST(CircularQueueTest, MultipleRound)
{
    const auto cq_size = 8;
    CircularQueue<uint32_t> cq(cq_size);

    // Filling the queue making it round multiple times
    auto items_added = cq_size * 3;
    for (auto idx = 0; idx < items_added; idx++) {
        cq.push_back(0);
    }

    auto starting_it = cq.begin();
    auto ending_it = cq.end();

    ASSERT_EQ(starting_it._round + 1, ending_it._round);
    ASSERT_EQ(ending_it - starting_it, cq_size);
}