#  Copyright 2020 Modelling, Simulation and Design Lab (MSDL) at
#  McGill University and the University of Antwerp (http://msdl.cs.mcgill.ca/)
#
#  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.

"""Tests the Queue building blocks to prove validity w.r.t. these tests.

Every test will run a PythonPDEVS simulation on a model. This model will
afterwards be analyzed against the expected measures. The model has a
general shape of

.. centered::
    ``TableGenerator -> Queue -> TableCollector``

Note:
    Whereas not all blocks seem to be tested, because the
    :class:`pypdevsbbl.generic.queues.Queue` is a coupled DEVS made up of other
    blocks, all will be tested in general.

    As far as the :class:`pypdevsbbl.generic.queues.QueueTracker` is concerned,
    the edgecases will not be tested, because it is only meant to be used
    in as a part of the :class:`pypdevsbbl.generic.queues.Queue`.

See Also:
    :mod:`pypdevsbbl.generic.queues`
"""

import unittest
from pypdevsbbl.generic.generators import TableGenerator
from pypdevsbbl.generic.queues import Queue, Retain, SimpleQueue, Advance
from pypdevsbbl.generic.collectors import TableCollector
from pypdevsbbl.generic.transformers import Transformer
from pypdevsbbl.extra.queues import LIFO

from pypdevs.DEVS import CoupledDEVS
from test.generic import simulate as _simulate, Model

class QModel(CoupledDEVS):
    """Simple model constructor for the tests.

    Args:
        table (list):   A list with all events that should happen.
        queue:          The queue to test.
        qport:          The input port of the queue.
        cport:          The output port of the queue.
    """
    def __init__(self, table, queue, qport, cport):
        super(QModel, self).__init__("Model")

        self.gen = self.addSubModel(TableGenerator("TableGenerator", table))
        self.queue = self.addSubModel(queue)
        self.coll = self.addSubModel(TableCollector("TableCollector"))

        self.connectPorts(self.gen.output, qport)
        self.connectPorts(cport, self.coll.input)


def simulate(table, queue, qport = None, cport = None):
    """Simulates the model.

    Args:
        table (list):   A list with all events that should happen.
        queue:          The queue to test.
        qport:          The input port of the queue.
        cport:          The output port of the queue.
    """
    if qport is None:
        qport = queue.enqueue
    if cport is None:
        cport = queue.dequeue
    model = QModel(table=table, queue=queue, qport=qport, cport=cport)
    _simulate(model)
    return model


class TestQueue(unittest.TestCase):
    def test_discipline(self):
        """Tests the queueing discipline (``fc``) of the :class:`pypdevsbbl.generic.queues.Queue`."""
        table = [
            (0, 7),
            (1, 3),
            (2, 8),
            (3, 1)
        ]

        queue = Queue("Queue", dd=1)
        model = simulate(table, queue)
        assert [x[1] for x in table] == [x[1] for x in model.coll.table()]

        queue = Queue("Queue", dd=5, fc=LIFO)
        model = simulate(table, queue)
        assert [x[1] for x in reversed(table)] == [x[1] for x in model.coll.table()]

        queue = Queue("Queue", dd=4, fc=lambda _, i, __: -i)
        model = simulate(table, queue)
        assert [x[1] for x in sorted(table, key=lambda x: x[1])] == [x[1] for x in model.coll.table()]

        queue = Queue("Queue", dd=4, fc=lambda _, i, __: i)
        model = simulate(table, queue)
        assert [x[1] for x in sorted(table, key=lambda x: x[1], reverse=True)] == [x[1] for x in model.coll.table()]

        queue = Queue("Queue", dd=2, fc=LIFO)
        model = simulate(table, queue)
        assert [3, 1, 8, 7] == [x[1] for x in model.coll.table()]

    def test_contents(self):
        """Tests the startup contents (``contents``) of the :class:`pypdevsbbl.generic.queues.SimpleQueue`."""
        table = [
            (0, 7),
            (1, 3),
            (0, 5),
            (2, 8),
            (0, 7),
            (0, 6),
            (1, 4),
            (3, 1),
            (1, 5)
        ]

        queue = SimpleQueue("Queue", contents=table)
        Q = queue.state["queue"]
        items = []
        while not Q.empty():
            items.append(Q.pop())
        assert [0] * len(table) == [i.arrival_time for i in items]
        assert table == [i.item for i in items]

    def test_reneging(self):
        """Tests the reneging of the :class:`pypdevsbbl.generic.queues.Queue`."""
        table = [
            (0, 7),
            (0, 3),
            (1, 8),
            (2, 1)
        ]

        queue = Queue("Queue", dr=1, dd=2)
        model = simulate(table, queue, cport=queue.renege)
        assert [x[1] for x in table] == [x[1] for x in model.coll.table()]

        queue = Queue("Queue", dr=1, dd=1)
        model = simulate(table, queue, cport=queue.renege)
        assert [x[1] for x in table] == [x[1] for x in model.coll.table()]

    def test_overflow(self):
        """Tests the overflow of the :class:`pypdevsbbl.generic.queues.Queue`."""
        table = [
            (0, 7),
            (1, 3),
            (2, 8),
            (3, 1),
            (4, 9),
            (5, 0),
            (6, 2),
            (7, 6)
        ]

        queue = Queue("Queue", K=3)
        model = simulate(table, queue, cport=queue.overflow)
        assert [1, 9, 0, 2, 6] == [x[1] for x in model.coll.table()]

        queue = Queue("Queue", K=0)
        model = simulate(table, queue, cport=queue.overflow)
        assert [x[1] for x in table] == [x[1] for x in model.coll.table()]

    def test_retain_individual(self):
        """Tests the :class:`pypdevsbbl.generic.queues.Retain` block."""
        table = [
            (0, 7),
            (1, 6),
            (2, 5),
            (3, 4)
        ]

        retain = Retain("Retain")
        model = simulate(table, retain, qport=retain.input, cport=retain.current)
        assert table == model.coll.table()

        retain = Retain("Retain")
        model = simulate(table, retain, qport=retain.input, cport=retain.previous)
        assert [
            (0, None),
            (1, 7),
            (2, 6),
            (3, 5)
        ] == model.coll.table()

    def test_retain_collection(self):
        """Tests the :class:`pypdevsbbl.generic.queues.Retain` block in *collection* mode."""
        table = [
            (0, 7),
            (0, 6),
            (0, 5),
            (1, 4),
            (1, 5)
        ]

        retain = Retain("Retain", False)
        model = simulate(table, retain, qport=retain.input, cport=retain.current)
        assert table == model.coll.table()

        retain = Retain("Retain", False)
        model = simulate(table, retain, qport=retain.input, cport=retain.previous)
        assert [[], [7], [7, 6], [], [4]] == [x[1] for x in model.coll.table()]

    def test_advance(self):
        """Tests the :class:`pypdevsbbl.generic.queues.Advance` block."""
        table = [
            (0, 7),
            (0, 6),
            (0, 5),
            (1, 4),
            (1, 8)
        ]
        nodes = [
            TableGenerator("C", table),
            Transformer("B", lambda i, e, t: 5),
            Advance("A"),
            TableCollector("D")
        ]
        conn = [
            (nodes[0].output, nodes[1].input),
            (nodes[1].original, nodes[2].input),
            (nodes[1].transformed, nodes[2].delay),
            (nodes[2].output, nodes[3].input)
        ]
        model = Model(nodes, conn)
        _simulate(model)

        assert [(x[0] + 5, x[1]) for x in table] == nodes[3].table()

if __name__ == '__main__':
    unittest.main()
