#!/usr/bin/env python
# SPDX-License-Identifier: ISC

#
# test_ospf6_topo2.py
# Part of NetDEF Topology Tests
#
# Copyright (c) 2021 by
# Network Device Education Foundation, Inc. ("NetDEF")
#

"""
test_ospf6_topo2.py: Test the FRR OSPFv3 daemon.
"""

import os
import sys
from functools import partial
import pytest

# Save the Current Working Directory to find configuration files.
CWD = os.path.dirname(os.path.realpath(__file__))
sys.path.append(os.path.join(CWD, "../"))

# pylint: disable=C0413
# Import topogen and topotest helpers
from lib import topotest
from lib.topogen import Topogen, TopoRouter, get_topogen
from lib.topolog import logger

# Required to instantiate the topology builder class.

pytestmark = [pytest.mark.ospf6d]


def expect_lsas(router, area, lsas, wait=5, extra_params=""):
    """
    Run the OSPFv3 show LSA database command and expect the supplied LSAs.

    Optional parameters:
     * `wait`: amount of seconds to wait.
     * `extra_params`: extra LSA database parameters.
     * `inverse`: assert the inverse of the expected.
    """
    tgen = get_topogen()

    command = "show ipv6 ospf6 database {} json".format(extra_params)

    logger.info("waiting OSPFv3 router '{}' LSA".format(router))
    test_func = partial(
        topotest.router_json_cmp,
        tgen.gears[router],
        command,
        {"areaScopedLinkStateDb": [{"areaId": area, "lsa": lsas}]},
    )
    _, result = topotest.run_and_expect(test_func, None, count=wait, wait=1)
    assertmsg = '"{}" convergence failure'.format(router)

    assert result is None, assertmsg


def expect_ospfv3_routes(router, routes, wait=5, type=None, detail=False):
    "Run command `ipv6 ospf6 route` and expect route with type."
    tgen = get_topogen()

    if detail == False:
        if type == None:
            cmd = "show ipv6 ospf6 route json"
        else:
            cmd = "show ipv6 ospf6 route {} json".format(type)
    else:
        if type == None:
            cmd = "show ipv6 ospf6 route detail json"
        else:
            cmd = "show ipv6 ospf6 route {} detail json".format(type)

    logger.info("waiting OSPFv3 router '{}' route".format(router))
    test_func = partial(
        topotest.router_json_cmp, tgen.gears[router], cmd, {"routes": routes}
    )
    _, result = topotest.run_and_expect(test_func, None, count=wait, wait=1)
    assertmsg = '"{}" convergence failure'.format(router)

    assert result is None, assertmsg


def dont_expect_route(router, unexpected_route, type=None):
    "Specialized test function to expect route go missing"
    tgen = get_topogen()

    if type == None:
        cmd = "show ipv6 ospf6 route json"
    else:
        cmd = "show ipv6 ospf6 route {} json".format(type)

    output = tgen.gears[router].vtysh_cmd(cmd, isjson=True)
    if unexpected_route in output["routes"]:
        return output["routes"][unexpected_route]
    return None


def build_topo(tgen):
    "Build function"

    # Create 4 routers
    for routern in range(1, 5):
        tgen.add_router("r{}".format(routern))

    switch = tgen.add_switch("s1")
    switch.add_link(tgen.gears["r1"])
    switch.add_link(tgen.gears["r2"])

    switch = tgen.add_switch("s2")
    switch.add_link(tgen.gears["r2"])
    switch.add_link(tgen.gears["r3"])

    switch = tgen.add_switch("s3")
    switch.add_link(tgen.gears["r2"])
    switch.add_link(tgen.gears["r4"])

    switch = tgen.add_switch("s4")
    switch.add_link(tgen.gears["r4"], nodeif="r4-stubnet")


def setup_module(mod):
    "Sets up the pytest environment"
    tgen = Topogen(build_topo, mod.__name__)
    tgen.start_topology()

    router_list = tgen.routers()
    for rname, router in router_list.items():

        daemon_file = "{}/{}/zebra.conf".format(CWD, rname)
        if os.path.isfile(daemon_file):
            router.load_config(TopoRouter.RD_ZEBRA, daemon_file)

        daemon_file = "{}/{}/ospf6d.conf".format(CWD, rname)
        if os.path.isfile(daemon_file):
            router.load_config(TopoRouter.RD_OSPF6, daemon_file)

    # Initialize all routers.
    tgen.start_router()


def test_wait_protocol_convergence():
    "Wait for OSPFv3 to converge"
    tgen = get_topogen()
    if tgen.routers_have_failure():
        pytest.skip(tgen.errors)

    logger.info("waiting for protocols to converge")

    def expect_neighbor_full(router, neighbor):
        "Wait until OSPFv3 convergence."
        logger.info("waiting OSPFv3 router '{}'".format(router))
        test_func = partial(
            topotest.router_json_cmp,
            tgen.gears[router],
            "show ipv6 ospf6 neighbor json",
            {"neighbors": [{"neighborId": neighbor, "state": "Full"}]},
        )
        _, result = topotest.run_and_expect(test_func, None, count=130, wait=1)
        assertmsg = '"{}" convergence failure'.format(router)
        assert result is None, assertmsg

    expect_neighbor_full("r1", "10.254.254.2")
    expect_neighbor_full("r2", "10.254.254.1")
    expect_neighbor_full("r2", "10.254.254.3")
    expect_neighbor_full("r2", "10.254.254.4")
    expect_neighbor_full("r3", "10.254.254.2")
    expect_neighbor_full("r4", "10.254.254.2")


def test_ospfv3_expected_route_types():
    "Test routers route type to determine if NSSA/Stub is working as expected."
    tgen = get_topogen()
    if tgen.routers_have_failure():
        pytest.skip(tgen.errors)

    logger.info("waiting for protocols to converge")

    def expect_ospf6_route_types(router, expected_summary):
        "Expect the correct route types."
        logger.info("waiting OSPFv3 router '{}'".format(router))
        test_func = partial(
            topotest.router_json_cmp,
            tgen.gears[router],
            "show ipv6 ospf6 route summary json",
            expected_summary,
        )
        _, result = topotest.run_and_expect(test_func, None, count=10, wait=1)
        assertmsg = '"{}" convergence failure'.format(router)
        assert result is None, assertmsg

    # Stub router: no external routes.
    expect_ospf6_route_types(
        "r1",
        {
            "numberOfIntraAreaRoutes": 1,
            "numberOfInterAreaRoutes": 3,
            "numberOfExternal1Routes": 0,
            "numberOfExternal2Routes": 0,
        },
    )
    # NSSA router: no external routes.
    expect_ospf6_route_types(
        "r4",
        {
            "numberOfIntraAreaRoutes": 1,
            "numberOfInterAreaRoutes": 2,
            "numberOfExternal1Routes": 0,
            "numberOfExternal2Routes": 3,
        },
    )


def test_ospf6_default_route():
    "Wait for OSPFv3 default route in stub area."
    tgen = get_topogen()
    if tgen.routers_have_failure():
        pytest.skip(tgen.errors)

    logger.info("waiting for default route")

    def expect_route(router, route, metric):
        "Test OSPF6 route existence."
        logger.info("waiting OSPFv3 router '{}' routes".format(router))
        test_func = partial(
            topotest.router_json_cmp,
            tgen.gears[router],
            "show ipv6 route json",
            {route: [{"metric": metric}]},
        )
        _, result = topotest.run_and_expect(test_func, None, count=5, wait=1)
        assertmsg = '"{}" convergence failure'.format(router)
        assert result is None, assertmsg

    metric = 123
    expect_lsas(
        "r1",
        "0.0.0.1",
        [{"prefix": "::/0", "metric": metric}],
        extra_params="inter-prefix detail",
    )
    expect_route("r1", "::/0", metric + 10)


def test_redistribute_metrics():
    """
    Test that the configured metrics are honored when a static route is
    redistributed.
    """
    tgen = get_topogen()
    if tgen.routers_have_failure():
        pytest.skip(tgen.errors)

    # Add new static route on r3.
    config = """
    configure terminal
    ipv6 route 2001:db8:500::/64 Null0
    """
    tgen.gears["r3"].vtysh_cmd(config)

    route = {
        "2001:db8:500::/64": {
            "metricType": 2,
            "metricCost": 10,
        }
    }
    logger.info(
        "Expecting AS-external route 2001:db8:500::/64 to show up with default metrics"
    )
    expect_ospfv3_routes("r2", route, wait=30, detail=True)

    # Change the metric of redistributed routes of the static type on r3.
    config = """
    configure terminal
    router ospf6
    redistribute static metric 50 metric-type 1
    """
    tgen.gears["r3"].vtysh_cmd(config)

    # Check if r3 reinstalled 2001:db8:500::/64 using the new metric type and value.
    route = {
        "2001:db8:500::/64": {
            "metricType": 1,
            "metricCost": 60,
        }
    }
    logger.info(
        "Expecting AS-external route 2001:db8:500::/64 to show up with updated metric type and value"
    )
    expect_ospfv3_routes("r2", route, wait=30, detail=True)


def test_nssa_lsa_type7():
    """
    Test that static route gets announced as external route when redistributed
    and gets removed when redistribution stops.
    """
    tgen = get_topogen()
    if tgen.routers_have_failure():
        pytest.skip(tgen.errors)

    #
    # Add new static route and check if it gets announced as LSA Type-7.
    #
    config = """
    configure terminal
    ipv6 route 2001:db8:100::/64 Null0
    """
    tgen.gears["r2"].vtysh_cmd(config)

    lsas = [
        {
            "type": "NSSA",
            "advertisingRouter": "10.254.254.2",
            "prefix": "2001:db8:100::/64",
            "forwardingAddress": "2001:db8:3::1",
        }
    ]
    route = {
        "2001:db8:100::/64": {
            "pathType": "E2",
            "nextHops": [{"nextHop": "::", "interfaceName": "r4-eth0"}],
        }
    }

    logger.info("Expecting LSA type-7 and OSPFv3 route 2001:db8:100::/64 to show up")
    expect_lsas("r4", "0.0.0.2", lsas, wait=30, extra_params="type-7 detail")
    expect_ospfv3_routes("r4", route, wait=30)

    #
    # Remove static route and check for LSA Type-7 removal.
    #
    config = """
    configure terminal
    no ipv6 route 2001:db8:100::/64 Null0
    """
    tgen.gears["r2"].vtysh_cmd(config)

    def dont_expect_lsa(unexpected_lsa):
        "Specialized test function to expect LSA go missing"
        output = tgen.gears["r4"].vtysh_cmd(
            "show ipv6 ospf6 database type-7 detail json", isjson=True
        )
        for lsa in output["areaScopedLinkStateDb"][0]["lsa"]:
            if lsa["prefix"] == unexpected_lsa["prefix"]:
                if lsa["forwardingAddress"] == unexpected_lsa["forwardingAddress"]:
                    return lsa
        return None

    logger.info("Expecting LSA type-7 and OSPFv3 route 2001:db8:100::/64 to go away")

    # Test that LSA doesn't exist.
    test_func = partial(dont_expect_lsa, lsas[0])
    _, result = topotest.run_and_expect(test_func, None, count=130, wait=1)
    assertmsg = '"{}" LSA still exists'.format("r4")
    assert result is None, assertmsg

    # Test that route doesn't exist.
    test_func = partial(dont_expect_route, "r4", "2001:db8:100::/64")
    _, result = topotest.run_and_expect(test_func, None, count=130, wait=1)
    assertmsg = '"{}" route still exists'.format("r4")
    assert result is None, assertmsg


def test_nssa_no_summary():
    """
    Test the following:
    * Type-3 inter-area routes should be removed when the NSSA no-summary option
      is configured;
    * A type-3 inter-area default route should be originated into the NSSA area
      when the no-summary option is configured;
    * Once the no-summary option is unconfigured, all previously existing
      Type-3 inter-area routes should be re-added, and the inter-area default
      route removed.
    """
    tgen = get_topogen()
    if tgen.routers_have_failure():
        pytest.skip(tgen.errors)

    #
    # Configure area 1 as a NSSA totally stub area.
    #
    config = """
    configure terminal
    router ospf6
    area 2 nssa no-summary
    """
    tgen.gears["r2"].vtysh_cmd(config)

    logger.info("Expecting inter-area routes to be removed")
    for route in ["2001:db8:1::/64", "2001:db8:2::/64"]:
        test_func = partial(dont_expect_route, "r4", route, type="inter-area")
        _, result = topotest.run_and_expect(test_func, None, count=130, wait=1)
        assertmsg = "{}'s {} inter-area route still exists".format("r4", route)
        assert result is None, assertmsg

    logger.info("Expecting inter-area default-route to be added")
    routes = {"::/0": {}}
    expect_ospfv3_routes("r4", routes, wait=30, type="inter-area")

    #
    # Configure area 1 as a regular NSSA area.
    #
    config = """
    configure terminal
    router ospf6
    area 2 nssa
    """
    tgen.gears["r2"].vtysh_cmd(config)

    logger.info("Expecting inter-area routes to be re-added")
    routes = {"2001:db8:1::/64": {}, "2001:db8:2::/64": {}}
    expect_ospfv3_routes("r4", routes, wait=30, type="inter-area")

    logger.info("Expecting inter-area default route to be removed")
    test_func = partial(dont_expect_route, "r4", "::/0", type="inter-area")
    _, result = topotest.run_and_expect(test_func, None, count=130, wait=1)
    assertmsg = "{}'s inter-area default route still exists".format("r4")
    assert result is None, assertmsg


def test_nssa_default_originate():
    """
    Test the following:
    * A type-7 default route should be originated into the NSSA area
      when the default-information-originate option is configured;
    * Once the default-information-originate option is unconfigured, the
      previously originated Type-7 default route should be removed.
    """
    tgen = get_topogen()
    if tgen.routers_have_failure():
        pytest.skip(tgen.errors)

    #
    # Configure r2 to announce a Type-7 default route.
    #
    config = """
    configure terminal
    router ospf6
    no default-information originate
    area 2 nssa default-information-originate
    """
    tgen.gears["r2"].vtysh_cmd(config)

    logger.info("Expecting Type-7 default-route to be added")
    routes = {"::/0": {}}
    expect_ospfv3_routes("r4", routes, wait=30, type="external-2")

    #
    # Configure r2 to stop announcing a Type-7 default route.
    #
    config = """
    configure terminal
    router ospf6
    area 2 nssa
    """
    tgen.gears["r2"].vtysh_cmd(config)

    logger.info("Expecting Type-7 default route to be removed")
    test_func = partial(dont_expect_route, "r4", "::/0", type="external-2")
    _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
    assertmsg = "r4's Type-7 default route still exists"
    assert result is None, assertmsg


def test_area_filters():
    """
    Test ABR import/export filters.
    """
    tgen = get_topogen()
    if tgen.routers_have_failure():
        pytest.skip(tgen.errors)

    #
    # Configure import/export filters on r2 (ABR for area 2).
    #
    config = """
    configure terminal
    ipv6 access-list ACL_IMPORT seq 5 permit 2001:db8:2::/64
    ipv6 access-list ACL_IMPORT seq 10 deny any
    ipv6 access-list ACL_EXPORT seq 10 deny any
    router ospf6
    area 1 import-list ACL_IMPORT
    area 1 export-list ACL_EXPORT
    """
    tgen.gears["r2"].vtysh_cmd(config)

    logger.info("Expecting inter-area routes to be removed on r1")
    for route in ["::/0", "2001:db8:3::/64"]:
        test_func = partial(dont_expect_route, "r1", route, type="inter-area")
        _, result = topotest.run_and_expect(test_func, None, count=130, wait=1)
        assertmsg = "{}'s {} inter-area route still exists".format("r1", route)
        assert result is None, assertmsg

    logger.info("Expecting inter-area routes to be removed on r3")
    for route in ["2001:db8:1::/64"]:
        test_func = partial(dont_expect_route, "r3", route, type="inter-area")
        _, result = topotest.run_and_expect(test_func, None, count=130, wait=1)
        assertmsg = "{}'s {} inter-area route still exists".format("r3", route)
        assert result is None, assertmsg

    #
    # Update the ACLs used by the import/export filters.
    #
    config = """
    configure terminal
    ipv6 access-list ACL_IMPORT seq 6 permit 2001:db8:3::/64
    ipv6 access-list ACL_EXPORT seq 5 permit 2001:db8:1::/64
    """
    tgen.gears["r2"].vtysh_cmd(config)

    logger.info("Expecting 2001:db8:3::/64 to be re-added on r1")
    routes = {"2001:db8:3::/64": {}}
    expect_ospfv3_routes("r1", routes, wait=30, type="inter-area")
    logger.info("Expecting 2001:db8:1::/64 to be re-added on r3")
    routes = {"2001:db8:1::/64": {}}
    expect_ospfv3_routes("r3", routes, wait=30, type="inter-area")

    #
    # Unconfigure r2's ABR import/export filters.
    #
    config = """
    configure terminal
    router ospf6
    no area 1 import-list ACL_IMPORT
    no area 1 export-list ACL_EXPORT
    """
    tgen.gears["r2"].vtysh_cmd(config)

    logger.info("Expecting ::/0 to be re-added on r1")
    routes = {"::/0": {}}
    expect_ospfv3_routes("r1", routes, wait=30, type="inter-area")


def test_nssa_range():
    """
    Test NSSA ABR ranges.
    """
    tgen = get_topogen()
    if tgen.routers_have_failure():
        pytest.skip(tgen.errors)

    # Configure new addresses on r4 and enable redistribution of connected
    # routes.
    config = """
    configure terminal
    interface r4-stubnet
    ipv6 address 2001:db8:1000::1/128
    ipv6 address 2001:db8:1000::2/128
    router ospf6
    redistribute connected
    """
    tgen.gears["r4"].vtysh_cmd(config)
    logger.info("Expecting NSSA-translated external routes to be added on r3")
    routes = {"2001:db8:1000::1/128": {}, "2001:db8:1000::2/128": {}}
    expect_ospfv3_routes("r3", routes, wait=30, type="external-2")

    # Configure an NSSA range on r2 (ABR for area 2).
    config = """
    configure terminal
    router ospf6
    area 2 nssa range 2001:db8:1000::/64
    """
    tgen.gears["r2"].vtysh_cmd(config)
    logger.info("Expecting summarized routes to be removed from r3")
    for route in ["2001:db8:1000::1/128", "2001:db8:1000::2/128"]:
        test_func = partial(dont_expect_route, "r3", route, type="external-2")
        _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
        assertmsg = "{}'s {} summarized route still exists".format("r3", route)
        assert result is None, assertmsg
    logger.info("Expecting NSSA range to be added on r3")
    routes = {
        "2001:db8:1000::/64": {
            "metricType": 2,
            "metricCost": 20,
            "metricCostE2": 10,
        }
    }
    expect_ospfv3_routes("r3", routes, wait=30, type="external-2", detail=True)

    # Change the NSSA range cost.
    config = """
    configure terminal
    router ospf6
    area 2 nssa range 2001:db8:1000::/64 cost 1000
    """
    tgen.gears["r2"].vtysh_cmd(config)
    logger.info("Expecting NSSA range to be updated with new cost")
    routes = {
        "2001:db8:1000::/64": {
            "metricType": 2,
            "metricCost": 20,
            "metricCostE2": 1000,
        }
    }
    expect_ospfv3_routes("r3", routes, wait=30, type="external-2", detail=True)

    # Configure the NSSA range to not be advertised.
    config = """
    configure terminal
    router ospf6
    area 2 nssa range 2001:db8:1000::/64 not-advertise
    """
    tgen.gears["r2"].vtysh_cmd(config)
    logger.info("Expecting NSSA summary route to be removed")
    route = "2001:db8:1000::/64"
    test_func = partial(dont_expect_route, "r3", route, type="external-2")
    _, result = topotest.run_and_expect(test_func, None, count=30, wait=1)
    assertmsg = "{}'s {} NSSA summary route still exists".format("r3", route)
    assert result is None, assertmsg

    # Remove the NSSA range.
    config = """
    configure terminal
    router ospf6
    no area 2 nssa range 2001:db8:1000::/64
    """
    tgen.gears["r2"].vtysh_cmd(config)
    logger.info("Expecting previously summarized routes to be re-added")
    routes = {
        "2001:db8:1000::1/128": {
            "metricType": 2,
            "metricCostE2": 20,
        },
        "2001:db8:1000::2/128": {
            "metricType": 2,
            "metricCostE2": 20,
        },
    }
    expect_ospfv3_routes("r3", routes, wait=30, type="external-2", detail=True)


def teardown_module(_mod):
    "Teardown the pytest environment"
    tgen = get_topogen()
    tgen.stop_topology()


def test_memory_leak():
    "Run the memory leak test and report results."
    tgen = get_topogen()
    if not tgen.is_memleak_enabled():
        pytest.skip("Memory leak test/report is disabled")

    tgen.report_memory_leaks()


if __name__ == "__main__":
    args = ["-s"] + sys.argv[1:]
    sys.exit(pytest.main(args))
