# Copyright 2015 ETH Zurich
#
# 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.
"""
:mod:`lib_path_store_test` --- lib.path_store unit tests
==========================================================================
"""
# Stdlib
import math
from unittest.mock import patch, MagicMock

# External packages
import nose
import nose.tools as ntools

# SCION
from lib.errors import SCIONPathPolicyViolated
from lib.packet.pcb import PathSegment
from lib.path_store import (
    PathPolicy,
    PathStore,
    PathStoreRecord
)
from test.testcommon import create_mock, create_mock_full


class TestPathPolicyCheckFilters(object):
    """
    Unit tests for lib.path_store.PathPolicy.check_filters
    """
    def _setup(self, unwanted=None, reasons=None, remote_ia=None):
        inst = PathPolicy()
        inst._check_unwanted_ases = create_mock()
        inst._check_unwanted_ases.return_value = unwanted
        inst._check_property_ranges = create_mock()
        inst._check_property_ranges.return_value = reasons
        inst._check_remote_ifid = create_mock()
        inst._check_remote_ifid.return_value = remote_ia
        pcb = create_mock(["short_desc"], class_=PathSegment)
        return inst, pcb

    def test_basic(self):
        inst, pcb = self._setup()
        # Call
        inst.check_filters(pcb)

    def test_unwanted_ases(self):
        inst, pcb = self._setup("unwanted AS")
        # Call
        ntools.assert_raises(SCIONPathPolicyViolated, inst.check_filters, pcb)

    def test_property_ranges(self):
        inst, pcb = self._setup(reasons="reasons")
        ntools.assert_raises(SCIONPathPolicyViolated, inst.check_filters, pcb)


class TestPathPolicyCheckPropertyRanges(object):
    """
    Unit tests for lib.path_store.PathPolicy._check_property_ranges
    """
    def _setup(self, max_bw=20):
        inst = PathPolicy()
        inst.property_ranges = {
            'PeerLinks': [0, 1], 'HopsLength': [0, 1], 'DelayTime': [0, 1],
            'GuaranteedBandwidth': [0, max_bw],
            'AvailableBandwidth': [0, max_bw], 'TotalBandwidth': [0, max_bw]
        }
        pcb = create_mock(["get_n_peer_links", "get_n_hops", "get_timestamp"])
        return inst, pcb

    @patch("lib.path_store.SCIONTime.get_time", new_callable=create_mock)
    def test_success(self, get_time):
        inst, pcb = self._setup()
        pcb.get_n_peer_links.return_value = 0.5
        pcb.get_n_hops.return_value = 0.5
        pcb.get_timestamp.return_value = 0.5
        # Call
        ntools.eq_(inst._check_property_ranges(pcb), [])

    @patch("lib.path_store.SCIONTime.get_time", new_callable=create_mock)
    def test_failure(self, get_time):
        inst, pcb = self._setup(max_bw=9)
        pcb.get_n_peer_links.return_value = 2
        pcb.get_n_hops.return_value = -1
        pcb.get_timestamp.return_value = -0.1
        # Call
        ntools.eq_(len(inst._check_property_ranges(pcb)), 6)

    @patch("lib.path_store.SCIONTime.get_time", new_callable=create_mock)
    def test_no_checks(self, get_time):
        inst, pcb = self._setup(max_bw=9)
        for key in inst.property_ranges:
            inst.property_ranges[key] = []
        pcb.get_n_peer_links.return_value = 2
        pcb.get_n_hops.return_value = -1
        pcb.get_timestamp.return_value = -0.1
        # Call
        ntools.eq_(inst._check_property_ranges(pcb), [])


class TestPathPolicyParseDict(object):
    """
    Unit tests for lib.path_store.PathPolicy.parse_dict
    """
    def test_basic(self):
        dict_ = {}
        dict_['BestSetSize'] = "best_set_size"
        dict_['CandidatesSetSize'] = "candidates_set_size"
        dict_['HistoryLimit'] = "history_limit"
        dict_['UpdateAfterNumber'] = "update_after_number"
        dict_['UpdateAfterTime'] = "update_after_time"
        dict_['UnwantedASes'] = "1-11,2-12"
        dict_['PropertyRanges'] = {'key1': "1-11", 'key2': "2-12"}
        dict_['PropertyWeights'] = "property_weights"
        pth_pol2 = PathPolicy()
        pth_pol2.parse_dict(dict_)
        ntools.eq_(pth_pol2.best_set_size, "best_set_size")
        ntools.eq_(pth_pol2.candidates_set_size, "candidates_set_size")
        ntools.eq_(pth_pol2.history_limit, "history_limit")
        ntools.eq_(pth_pol2.update_after_number, "update_after_number")
        ntools.eq_(pth_pol2.update_after_time, "update_after_time")
        ntools.eq_(pth_pol2.property_ranges, {'key1': (1, 11), 'key2': (2, 12)})
        ntools.eq_(pth_pol2.property_weights, "property_weights")


class TestPathStoreRecordInit(object):
    """
    Unit tests for lib.path_store.PathStoreRecord.__init__
    """
    @patch("lib.path_store.PathStoreRecord.update", autospec=True)
    @patch("lib.path_store.SCIONTime.get_time", new_callable=create_mock)
    def test(self, get_time, update):
        pcb = create_mock(['get_hops_hash', 'get_n_hops', 'get_n_peer_links'],
                          class_=PathSegment)
        get_time.return_value = PathStoreRecord.DEFAULT_OFFSET + 1
        # Call
        inst = PathStoreRecord(pcb)
        # Tests
        ntools.eq_(inst.id, pcb.get_hops_hash.return_value)
        ntools.eq_(inst.peer_links, pcb.get_n_peer_links.return_value)
        ntools.eq_(inst.hops_length, pcb.get_n_hops.return_value)
        ntools.eq_(inst.fidelity, 0)
        ntools.eq_(inst.disjointness, 0)
        ntools.eq_(inst.last_sent_time, 1)
        ntools.eq_(inst.guaranteed_bandwidth, 0)
        ntools.eq_(inst.available_bandwidth, 0)
        ntools.eq_(inst.total_bandwidth, 0)
        update.assert_called_once_with(inst, pcb)


class TestPathStoreRecordUpdate(object):
    """
    Unit tests for lib.path_store.PathStoreRecord.update
    """
    @patch("lib.path_store.SCIONTime.get_time", new_callable=create_mock)
    @patch("lib.path_store.PathStoreRecord.__init__", autospec=True,
           return_value=None)
    def test(self, init, get_time):
        inst = PathStoreRecord("pcb")
        get_time.return_value = 100
        pcb = create_mock(["copy", "get_hops_hash", "get_timestamp",
                           "get_expiration_time"])
        inst.id = pcb.get_hops_hash.return_value
        pcb.get_timestamp.return_value = 95
        # Call
        inst.update(pcb)
        # Tests
        pcb.copy.assert_called_once_with()
        ntools.eq_(inst.delay_time, 5)
        ntools.eq_(inst.last_seen_time, 100)
        ntools.eq_(inst.expiration_time, pcb.get_expiration_time.return_value)


class TestPathStoreRecordUpdateFidelity(object):
    """
    Unit tests for lib.path_store.PathStoreRecord.update_fidelity
    """
    @patch("lib.path_store.SCIONTime.get_time", new_callable=create_mock)
    @patch("lib.path_store.PathStoreRecord.__init__", autospec=True,
           return_value=None)
    def test_basic(self, init, time_):
        path_policy = PathPolicy()
        path_policy.property_weights['PeerLinks'] = 10
        path_policy.property_weights['HopsLength'] = 1
        path_policy.property_weights['Disjointness'] = 2
        path_policy.property_weights['LastSentTime'] = 3