"""
Test base helpers.
"""

# Python std lib
import os
import sys
import unittest
import textwrap

# third party libs
try:
    import jinja2  # noqa

    HAS_JINJA = True
except ImportError:
    HAS_JINJA = False

try:
    import textfsm  # noqa

    HAS_TEXTFSM = True
except ImportError:
    HAS_TEXTFSM = False

try:
    from lxml import etree as ET

    HAS_LXML = True
except ImportError:
    HAS_LXML = False

try:
    from netaddr.core import AddrFormatError

    HAS_NETADDR = True
except ImportError:
    HAS_NETADDR = False

try:
    from ttp import ttp  # noqa

    HAS_TTP = True
except ImportError:
    HAS_TTP = False

try:
    from ttp_templates import get_template  # noqa

    HAS_TTP_TEMPLATES = True
except ImportError:
    HAS_TTP_TEMPLATES = False

# NAPALM base
import napalm.base.helpers
import napalm.base.constants as C
from napalm.base.netmiko_helpers import netmiko_args
import napalm.base.exceptions
from napalm.base.base import NetworkDriver
from napalm.base.utils.string_parsers import (
    convert_uptime_string_seconds,
    parse_fixed_width,
)


class TestBaseHelpers(unittest.TestCase):
    """Test helpers functions."""

    @classmethod
    def setUpClass(cls):
        cls.network_driver = FakeNetworkDriver()
        # neded when calling helpers

    def test_load_template(self):
        """
        Tests the helper function ```load_template```:

            * check if raises TemplateNotImplemented when trying to use inexisting template
            * check if can load empty template
            * check if raises TemplateRenderException when template is not correctly formatted
            * check if can load correct template
            * check if raises IOError if invalid path is specified
            * check if raises TemplateNotImplemented when trying to use inexisting template in
              custom path
            * check if can load correct template from custom path
            * check if template passed as string can be loaded
            * check that the search path setup by MRO is correct when loading an incorrect template
        """

        self.assertTrue(HAS_JINJA)  # firstly check if jinja2 is installed
        _NTP_PEERS_LIST = ["172.17.17.1", "172.17.17.2"]
        _TEMPLATE_VARS = {"peers": _NTP_PEERS_LIST}

        self.assertRaises(
            napalm.base.exceptions.TemplateNotImplemented,
            napalm.base.helpers.load_template,
            self.network_driver,
            "__this_template_does_not_exist__",
            **_TEMPLATE_VARS
        )

        self.assertTrue(
            napalm.base.helpers.load_template(
                self.network_driver, "__empty_template__", **_TEMPLATE_VARS
            )
        )

        self.assertRaises(
            napalm.base.exceptions.TemplateRenderException,
            napalm.base.helpers.load_template,
            self.network_driver,
            "__completely_wrong_template__",
            **_TEMPLATE_VARS
        )

        self.assertTrue(
            napalm.base.helpers.load_template(
                self.network_driver, "__a_very_nice_template__", **_TEMPLATE_VARS
            )
        )

        self.assertRaises(
            IOError,
            napalm.base.helpers.load_template,
            self.network_driver,
            "__a_very_nice_template__",
            template_path="/this/path/does/not/exist",
            **_TEMPLATE_VARS
        )

        install_dir = os.path.dirname(
            os.path.abspath(sys.modules[self.network_driver.__module__].__file__)
        )
        custom_path = os.path.join(install_dir, "../custom/path/base")

        self.assertRaises(
            napalm.base.exceptions.TemplateNotImplemented,
            napalm.base.helpers.load_template,
            self.network_driver,
            "__this_template_does_not_exist__",
            template_path=custom_path,
            **_TEMPLATE_VARS
        )

        self.assertTrue(
            napalm.base.helpers.load_template(
                self.network_driver,
                "__a_very_nice_template__",
                template_path=custom_path,
                **_TEMPLATE_VARS
            )
        )

        template_source = "{% for peer in peers %}ntp peer {{peer}}\n{% endfor %}"

        self.assertTrue(
            napalm.base.helpers.load_template(
                self.network_driver,
                "_this_still_needs_a_name",
                template_source=template_source,
                **_TEMPLATE_VARS
            )
        )

        def __foo_to_bar(s):
            if s == "foo":
                return "bar"
            return s

        jinja_filters = {"foo_to_bar": __foo_to_bar}

        self.assertTrue(
            napalm.base.helpers.load_template(
                self.network_driver,
                "__custom_jinja_filter_template__",
                jinja_filters=jinja_filters,
                **_TEMPLATE_VARS
            )
        )
        # MIGRATION mircea
        #  self.assertRaisesRegexp(napalm.base.exceptions.TemplateNotImplemented,
        #                          "path.*napalm-base/test/unit/templates'" +
        #                          ",.*napalm-base/napalm.base/templates']",
        #                          napalm.base.helpers.load_template,
        #                          self.network_driver,
        #                          '__this_template_does_not_exist__',
        #                          **_TEMPLATE_VARS)

    def test_textfsm_extractor(self):
        """
        Tests the helper function ```textfsm_extractor```:

            * check if raises TemplateNotImplemented when template is not defined
            * check if raises TemplateRenderException when template is empty
            * check if raises TemplateRenderException when template is not properly defined
            * check if returns a non-empty list as output
        """

        self.assertTrue(
            HAS_TEXTFSM
        )  # before anything else, let's see if TextFSM is available
        _TEXTFSM_TEST_STRING = """
        Groups: 3 Peers: 3 Down peers: 0
        Table          Tot Paths  Act Paths Suppressed    History Damp State    Pending
        inet.0               947        310          0          0          0          0
        inet6.0              849        807          0          0          0          0
        Peer                     AS      InPkt     OutPkt    OutQ   Flaps Last Up/Dwn State|#Active/Received/Damped...  #noqa
        10.247.68.182         65550     131725   28179233       0      11     6w3d17h Establ
          inet.0: 4/5/1
          inet6.0: 0/0/0
        10.254.166.246        65550     136159   29104942       0       0      6w5d6h Establ
          inet.0: 0/0/0
          inet6.0: 7/8/1
        192.0.2.100           65551    1269381    1363320       0       1      9w5d6h 2/3/0 0/0/0
        """

        self.assertRaises(
            napalm.base.exceptions.TemplateNotImplemented,
            napalm.base.helpers.textfsm_extractor,
            self.network_driver,
            "__this_template_does_not_exist__",
            _TEXTFSM_TEST_STRING,
        )

        self.assertRaises(
            napalm.base.exceptions.TemplateRenderException,
            napalm.base.helpers.textfsm_extractor,
            self.network_driver,
            "__empty_template__",
            _TEXTFSM_TEST_STRING,
        )

        self.assertRaises(
            napalm.base.exceptions.TemplateRenderException,
            napalm.base.helpers.textfsm_extractor,
            self.network_driver,
            "__completely_wrong_template__",
            _TEXTFSM_TEST_STRING,
        )

        self.assertIsInstance(
            napalm.base.helpers.textfsm_extractor(
                self.network_driver, "__a_very_nice_template__", _TEXTFSM_TEST_STRING
            ),
            list,
        )

    def test_convert(self):
        """
        Tests helper function ```convert```:

            * cast of non-int str to int returns default value
            * cast of str to float returns desired float-type value
            * cast of None obj to string does not cast, but returns default
        """
        self.assertTrue(
            napalm.base.helpers.convert(int, "non-int-value", default=-100) == -100
        )
        # default value returned
        self.assertIsInstance(napalm.base.helpers.convert(float, "1e-17", 1.0), float)
        # converts indeed to float
        self.assertFalse(napalm.base.helpers.convert(str, None, "") == "None")
        # should not convert None-type to 'None' string
        self.assertTrue(napalm.base.helpers.convert(str, None, "") == "")
        # should return empty unicode

    def test_find_txt(self):
        """
        Tests helper function ```find_txt```:

            * check if returns default when retrieving from wrong path
            * check if non-empty string inside existing tag
            * check if can parse boolean values as text
            * check if can retrieve int values
        """

        self.assertTrue(HAS_LXML)  # firstly check if lxml is installed

        _XML_STRING = """
        <family>
            <parent1>
                <child1>
                    Hi, I am child1 of parent1.
                </child1>
                <child2>
                    Hi, I am child2 of parent1.
                </child2>
                <child3>
                    Hi, I am child3 of parent1.
                </child3>
            </parent1>
            <parent2>
                <child1>
                    Hi, I am child1 of parent2.
                </child1>
                <child2 special="true">
                    Haha.
                </child2>
            </parent2>
            <parent3 lonely="true"></parent3>
            <stats>
                <parents>3</parents>
                <children>4</children>
            </stats>
        </family>
        """

        _XML_TREE = ET.fromstring(_XML_STRING)

        self.assertFalse(
            napalm.base.helpers.find_txt(_XML_TREE, "parent100/child200", False)
        )
        # returns default value (in this case boolean value False)

        # check if content inside the tag /parent1/child1
        self.assertTrue(
            len(napalm.base.helpers.find_txt(_XML_TREE, "parent1/child1")) > 0
        )

        # check if able to eval boolean returned as text inside the XML tree
        self.assertTrue(
            eval(
                napalm.base.helpers.find_txt(
                    _XML_TREE, "parent3/@lonely", "false"
                ).title()
            )
        )

        # int values
        self.assertIsInstance(
            int(napalm.base.helpers.find_txt(_XML_TREE, "stats/parents")), int
        )

        # get first match of the tag child3, wherever would be
        _CHILD3_TAG = _XML_TREE.find(".//child3")

        # check if content inside the discovered tag child3
        self.assertTrue(len(napalm.base.helpers.find_txt(_CHILD3_TAG, ".")) > 0)

        _SPECIAL_CHILD2 = _XML_TREE.find('.//child2[@special="true"]')

        self.assertTrue(len(napalm.base.helpers.find_txt(_SPECIAL_CHILD2, ".")) > 0)

        _SPECIAL_CHILD100 = _XML_TREE.find('.//child100[@special="true"]')

        self.assertFalse(len(napalm.base.helpers.find_txt(_SPECIAL_CHILD100, ".")) > 0)

        _NOT_SPECIAL_CHILD2 = _XML_TREE.xpath('.//child2[not(@special="true")]')[0]
        # use XPath to get tags using predicates!

        self.assertTrue(len(napalm.base.helpers.find_txt(_NOT_SPECIAL_CHILD2, ".")) > 0)

    def test_mac(self):
        """
        Tests the helper function ```mac```:

            * check if raises AddrFormatError when invalid MAC
            * check if MAC address returned as expected
        """

        self.assertTrue(HAS_NETADDR)

        # test that raises AddrFormatError when wrong format
        self.assertRaises(AddrFormatError, napalm.base.helpers.mac, "fake")

        self.assertEqual(napalm.base.helpers.mac("0123456789ab"), "01:23:45:67:89:AB")
        self.assertEqual(napalm.base.helpers.mac("0123.4567.89ab"), "01:23:45:67:89:AB")
        self.assertEqual(napalm.base.helpers.mac("123.4567.89ab"), "01:23:45:67:89:AB")

    def test_ip(self):
        """
        Tests the helper function ```ip```:

            * check if raises AddrFormatError when invalid IP address
            * check if calls using incorrect version raises ValueError
            * check if IPv6 address returned as expected
        """

        # test that raises ValueError when wrong format
        self.assertRaises(ValueError, napalm.base.helpers.ip, "fake")
        self.assertRaises(
            ValueError,
            napalm.base.helpers.ip,
            "2001:db8:85a3::8a2e:370:7334",
            version=4,
        )
        self.assertRaises(ValueError, napalm.base.helpers.ip, "192.168.17.1", version=6)
        self.assertEqual(
            napalm.base.helpers.ip("2001:0dB8:85a3:0000:0000:8A2e:0370:7334"),
            "2001:db8:85a3::8a2e:370:7334",
        )
        self.assertEqual(
            napalm.base.helpers.ip("2001:0DB8::0003", version=6), "2001:db8::3"
        )

    def test_as_number(self):
        """Test the as_number helper function."""
        self.assertEqual(napalm.base.helpers.as_number("64001"), 64001)
        self.assertEqual(napalm.base.helpers.as_number("1.0"), 65536)
        self.assertEqual(napalm.base.helpers.as_number("1.100"), 65636)
        self.assertEqual(napalm.base.helpers.as_number("1.65535"), 131071)
        self.assertEqual(napalm.base.helpers.as_number("65535.65535"), 4294967295)
        self.assertEqual(napalm.base.helpers.as_number(64001), 64001)

    def test_convert_uptime_string_seconds(self):
        """
        Tests the parser function ```convert_uptime_string_seconds```:

            * check if all raw uptime strings passed return the expected uptime in seconds
        """

        # Regex 1
        self.assertEqual(
            convert_uptime_string_seconds("24 days,  11 hours,  25 minutes"), 2114700
        )
        self.assertEqual(convert_uptime_string_seconds("1 hour,  5 minutes"), 3900)
        self.assertEqual(
            convert_uptime_string_seconds("1 year,  2 weeks, 5 minutes"), 32745900
        )
        self.assertEqual(
            convert_uptime_string_seconds("95 weeks, 2 days, 10 hours, 58 minutes"),
            57668280,
        )
        self.assertEqual(
            convert_uptime_string_seconds("26 weeks, 2 days, 7 hours, 7 minutes"),
            15923220,
        )
        self.assertEqual(
            convert_uptime_string_seconds("19 weeks, 2 days, 2 hours, 2 minutes"),
            11671320,
        )
        self.assertEqual(
            convert_uptime_string_seconds("15 weeks, 3 days, 5 hours, 57 minutes"),
            9352620,
        )
        self.assertEqual(
            convert_uptime_string_seconds("1 year, 8 weeks, 15 minutes"), 36375300
        )
        self.assertEqual(
            convert_uptime_string_seconds("8 weeks, 2 hours, 5 minutes"), 4845900
        )
        self.assertEqual(
            convert_uptime_string_seconds("8 weeks, 2 hours, 1 minute"), 4845660
        )
        self.assertEqual(
            convert_uptime_string_seconds(
                "2 years, 40 weeks, 1 day, 22 hours, 3 minutes"
            ),
            87429780,
        )
        self.assertEqual(
            convert_uptime_string_seconds(
                "2 years, 40 weeks, 1 day, 19 hours, 46 minutes"
            ),
            87421560,
        )
        self.assertEqual(
            convert_uptime_string_seconds("1 year, 39 weeks, 15 hours, 23 minutes"),
            55178580,
        )
        self.assertEqual(
            convert_uptime_string_seconds("33 weeks, 19 hours, 12 minutes"), 20027520
        )
        self.assertEqual(
            convert_uptime_string_seconds("33 weeks, 19 hours, 8 minutes"), 20027280
        )
        self.assertEqual(
            convert_uptime_string_seconds("33 weeks, 19 hours, 10 minutes"), 20027400
        )
        self.assertEqual(
            convert_uptime_string_seconds("51 weeks, 5 days, 13 hours, 0 minutes"),
            31323600,
        )
        self.assertEqual(
            convert_uptime_string_seconds("51 weeks, 5 days, 12 hours, 57 minutes"),
            31323420,
        )
        self.assertEqual(
            convert_uptime_string_seconds("51 weeks, 5 days, 12 hours, 55 minutes"),
            31323300,
        )
        self.assertEqual(
            convert_uptime_string_seconds("51 weeks, 5 days, 12 hours, 58 minutes"),
            31323480,
        )

        # Regex 2
        self.assertEqual(convert_uptime_string_seconds("114 days, 22:27:32"), 9930452)
        self.assertEqual(convert_uptime_string_seconds("0 days, 22:27:32"), 80852)
        self.assertEqual(convert_uptime_string_seconds("365 days, 5:01:44"), 31554104)

        # Regex 3
        self.assertEqual(convert_uptime_string_seconds("7w6d5h4m3s"), 4770243)
        self.assertEqual(convert_uptime_string_seconds("95w2d10h58m"), 57668280)
        self.assertEqual(convert_uptime_string_seconds("1h5m"), 3900)

    def test_netmiko_arguments(self):
        """Test the netmiko argument processing."""
        self.assertEqual(netmiko_args(optional_args={}), {})

        test_case = {"secret": "whatever"}
        self.assertEqual(netmiko_args(test_case), test_case)

        test_case = {"secret": "whatever", "use_keys": True}
        self.assertEqual(netmiko_args(test_case), test_case)

        test_case = {
            "secret": "whatever",
            "use_keys": True,
            "ssh_config_file": "~/.ssh/config",
        }
        self.assertEqual(netmiko_args(test_case), test_case)

        test_case = {"secret": "whatever", "transport": "telnet"}
        self.assertEqual(netmiko_args(test_case), {"secret": "whatever"})

        test_case = {"secret": "whatever", "transport": "telnet", "port": 8022}
        self.assertEqual(netmiko_args(test_case), {"secret": "whatever", "port": 8022})

        test_case = {
            "secret": "",
            "port": None,
            "verbose": False,
            "global_delay_factor": 1,
            "use_keys": False,
            "key_file": None,
            "allow_agent": False,
            "ssh_strict": False,
            "system_host_keys": False,
            "alt_host_keys": False,
            "alt_key_file": "",
            "ssh_config_file": None,
            "session_timeout": 60,
            "blocking_timeout": 8,
            "keepalive": 0,
            "default_enter": None,
            "response_return": None,
            "serial_settings": None,
        }
        self.assertEqual(netmiko_args(test_case), test_case)

        test_case = {
            "inline_transfer": True,
            "transport": "ssh",
            "secret": "",
            "port": None,
            "verbose": False,
            "global_delay_factor": 1,
            "use_keys": False,
            "key_file": None,
            "allow_agent": False,
            "ssh_strict": False,
            "system_host_keys": False,
            "alt_host_keys": False,
            "alt_key_file": "",
            "ssh_config_file": None,
            "session_timeout": 60,
            "blocking_timeout": 8,
            "keepalive": 0,
            "default_enter": None,
            "response_return": None,
            "serial_settings": None,
        }
        result_dict = {}
        result_dict.update(test_case)
        result_dict.pop("inline_transfer")
        result_dict.pop("transport")
        self.assertEqual(netmiko_args(test_case), result_dict)

    def test_sanitized_config(self):
        config = textwrap.dedent(
            """\
            !
            version 15.5
            service timestamps debug datetime msec
            service timestamps log datetime msec
            no platform punt-keepalive disable-kernel-core
            platform console auto
            !
            hostname CSR1
            enable secret 5 $1$asdiud2982fp2f
            username root password 7 096f471a1a0a57213e2f2f19
            !
            redundancy
            radius-server host 10.124.24.18 auth-port 1812\
            acct-port 1813 key 7 09687b2a3245343b382f2b"""
        )
        expected = textwrap.dedent(
            """\
            !
            version 15.5
            service timestamps debug datetime msec
            service timestamps log datetime msec
            no platform punt-keepalive disable-kernel-core
            platform console auto
            !
            hostname CSR1
            enable secret 5 <removed>
            username root password 7 <removed>
            !
            redundancy
            radius-server host 10.124.24.18 auth-port 1812\
            acct-port 1813 key 7 <removed>"""
        )
        ret = napalm.base.helpers.sanitize_config(config, C.CISCO_SANITIZE_FILTERS)
        self.assertEqual(ret, expected)

    def test_ttp_parse(self):
        """
        Tests helper function ```ttp_parse```:

            * check if raises TemplateRenderException when template does not exist
            * check if raises TemplateRenderException for /utils/ttp_templates/bad_template.txt
            * check if returns expected result as output for ./utils/ttp_templates/good_template.txt
            * check if returns expected result as output for inline template
            * check if returns expected result as output for template from ttp templates
        """

        self.assertTrue(
            HAS_TTP, "Install TTP: python3 -m pip install ttp"
        )  # before anything else, let's see if TTP is available
        _TTP_TEST_STRING = """
interface Gi1/1
 description FOO
!
interface Gi1/2
 description BAR
!
        """
        _TTP_TEST_TEMPLATE = '<group>\ninterface {{ interface }}\n description {{ description | re(".+") }}\n! {{ _end_ }}\n</group>'  # noqa:E501
        _EXPECTED_RESULT = [
            {"description": "FOO", "interface": "Gi1/1"},
            {"description": "BAR", "interface": "Gi1/2"},
        ]
        self.assertRaises(
            napalm.base.exceptions.TemplateRenderException,
            napalm.base.helpers.ttp_parse,
            self.network_driver,
            "__this_template_does_not_exist__",
            _TTP_TEST_STRING,
        )

        self.assertRaises(
            napalm.base.exceptions.TemplateRenderException,
            napalm.base.helpers.ttp_parse,
            self.network_driver,
            "bad_template",
            _TTP_TEST_STRING,
        )

        # use ./utils/ttp_templates/good_template.txt
        result = napalm.base.helpers.ttp_parse(
            self.network_driver, "good_template", _TTP_TEST_STRING
        )
        self.assertEqual(result, _EXPECTED_RESULT)

        # use inline template
        result = napalm.base.helpers.ttp_parse(
            self.network_driver, _TTP_TEST_TEMPLATE, _TTP_TEST_STRING
        )
        self.assertEqual(result, _EXPECTED_RESULT)

        self.assertTrue(
            HAS_TTP_TEMPLATES,
            "Install TTP Templates: python3 -m pip install ttp-templates",
        )  # before anything else, let's see if TTP_Templates is available

        # use template from ttp templates package
        result = napalm.base.helpers.ttp_parse(
            self.network_driver,
            "ttp://platform/test_platform_show_run_pipe_sec_interface.txt",
            _TTP_TEST_STRING,
        )
        self.assertEqual(result, _EXPECTED_RESULT)

    def test_parse_fixed_width(self):
        _TEST_STRING = """   VRF              RD              Protocols       State            Interfaces
---------------- --------------- --------------- ------------------- ---------------------------
123456789012345671234567890123456123456789012345612345678901234567890123456789012345678901234567
"""  # noqa: E501
        _EXPECTED_RESULT = [
            (
                "   VRF           ",
                "   RD           ",
                "   Protocols    ",
                "   State            ",
                "Interfaces                 ",
            ),
            (
                "---------------- ",
                "--------------- ",
                "--------------- ",
                "------------------- ",
                "---------------------------",
            ),
            (
                "12345678901234567",
                "1234567890123456",
                "1234567890123456",
                "12345678901234567890",
                "123456789012345678901234567",
            ),
        ]
        result = parse_fixed_width(_TEST_STRING, 17, 16, 16, 20, 27)
        self.assertEqual(result, _EXPECTED_RESULT)


class FakeNetworkDriver(NetworkDriver):
    def __init__(self):
        """Connection details not needed."""
        pass

    def load_merge_candidate(self, config=None):
        """
        This method is called at the end of the helper ```load_template```.
        To check whether the test arrives at the very end of the helper function,
        will return True instead of raising NotImplementedError exception.
        """
        return True
