import asyncio
from collections import namedtuple
import logging
import os
import sys
import unittest

sys.path.append(os.path.dirname(__file__))  # let code under test load stubs
import ap
import olt
from preseem import NetworkMetadataReference, Reference
from preseem.network_element_update import NetworkElementUpdate
from preseem_protobuf.network_poller import network_poller_pb2
from fake_context import FakeContext

#logging.basicConfig(format='%(asctime)s %(levelname)s %(message)s',
#                        level=logging.INFO)

CpeRadio = namedtuple('CpeRadio', ('mac_address', 'dev_macs'))
class Ap(ap.Ap):
    """We subclass the Ap object to add some test synchronization helpers"""
    async def poll(self):
        FakeContext.ap_event.clear()
        await super().poll()
        FakeContext.ap_event.set()

class Olt(olt.Olt):
    """We subclass the Olt object to add some test synchronization helpers"""
    async def poll(self):
        FakeContext.ap_event.clear()
        await super().poll()
        FakeContext.ap_event.set()

class FakeClient:
    def __init__(self, host=None):
        self.dft = {}
        self.host = host

class FakeModule:
    def __init__(self):
        self.stations = []
        self.model = None
        self.mode = "ap"  # I'm not sure yet if this should be changed to olt.
        self.next_neu = NetworkElementUpdate()

    async def poll(self):
        self.network_element_update_msg = self.next_neu


class TestOLT(unittest.TestCase):
    def setUp(self):
        self.ctx = FakeContext()
        self.loop = asyncio.new_event_loop() # needed to initialize asyncio
        asyncio.set_event_loop(self.loop)
        self._await(self.ctx.start())

    def tearDown(self):
        self._await(self.ctx.close())
        self.loop.close()

    def _await(self, co):
        return self.loop.run_until_complete(co)

    def test_no_data(self):
        """Test polling of an element that does not return a NEU message."""
        olt = Olt(self.ctx, 'TestAP', 'TestAP', 'MySite', FakeClient('MyHost'), None, 'olt', None)
        olt.module = FakeModule()
        self._await(olt.load())
        olt.module.next_neu = None
        self._await(olt.load())  # we're just making sure this doesn't crash

    def test_olt_poll(self):
        """Test polling of an OLT element type."""
        olt = Olt(self.ctx, 'TestAP', 'TestAP', 'MySite', FakeClient('MyHost'), None, 'olt', None)
        olt.module = FakeModule()
        self._await(olt.load())

        # For now we're just testing some very basic functionality.
        topology1_snmp_refs = self.ctx.netmeta_model.refs.get('topology1_snmp') or {}
        neu = getattr(olt, 'network_element_update_msg', None)
        self.assertEqual(len(topology1_snmp_refs), 1)
        self.assertIsNotNone(neu)

    def test_new_pon(self):
        """Network metadata is setup correctly for a PON."""
        olt = Olt(self.ctx, 'TestAP', 'TestAP', 'MySite', FakeClient('MyHost'), None, 'olt', None)
        olt.module = FakeModule()
        pon_port = olt.module.next_neu.data.interfaces.add()
        pon = pon_port.pons.add()
        pon.id = '100001'
        pon.name = 'GPON 100001'
        self._await(olt.load())
        exp_ref = NetworkMetadataReference(type='ap', value='TestAP.100001', attributes={'topology1': 'GPON 100001', 'topology2': 'OLT.TestAP', 'ap_config': 'TestAP.100001', 'topology1_snmp': 'TestAP', 'system': 'net'})
        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 1)
        self.assertEqual(ap_refs.get('TestAP.100001'), exp_ref)

    def test_del_pon(self):
        """Network metadata is updated when a PON is removed or when an OLT goes offline."""
        olt = Olt(self.ctx, 'TestAP', 'TestAP', 'MySite', FakeClient('MyHost'), None, 'olt', None)
        olt.module = FakeModule()
        slot_1 = olt.module.next_neu.data.modules.add()
        slot_2 = olt.module.next_neu.data.modules.add()
        port_1_1 = slot_1.interfaces.add()
        port_1_2 = slot_1.interfaces.add()
        port_2_1 = slot_2.interfaces.add()
        port_2_2 = slot_2.interfaces.add()
        pon_1_1_1 = port_1_1.pons.add()
        pon_1_1_1.id = '1/1/1 GPON'
        pon_1_1_2 = port_1_1.pons.add()
        pon_1_1_2.id = '1/1/1 XGS-PON'
        pon_1_2_1 = port_1_2.pons.add()
        pon_1_2_1.id = '1/1/2 GPON'
        pon_2_1_1 = port_2_1.pons.add()
        pon_2_1_1.id = '1/2/1 GPON'
        pon_2_1_2 = port_2_1.pons.add()
        pon_2_1_2.id = '1/2/1 XGS-PON'
        pon_2_2_1 = port_2_2.pons.add()
        pon_2_2_1.id = '1/2/2 GPON'
        self._await(olt.load())
        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 6)

        # Simulate an OLT line card being removed
        del olt.module.next_neu.data.modules[1]
        self._await(olt.load())
        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 3)

        # Delete the other card
        card2 = olt.module.next_neu.data.modules[0]
        del olt.module.next_neu.data.modules[0]
        self._await(olt.load())
        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 0)

        # Reinsert the card in slot 2
        olt.module.next_neu.data.modules.append(card2)
        self._await(olt.load())
        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 3)

        # Test the refs are removed if the element goes offline
        olt.offline = True
        self._await(olt.remove_ref_offline())
        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 0)

    def test_olt_bootstrap(self):
        """An OLT reads its previous state on initialization."""
        # Create existing state (that will no longer be present)
        cur_ref = NetworkMetadataReference(type='ap', value='TestAP.100001', attributes={'topology1': 'GPON 100001', 'topology2': 'OLT.TestAP', 'ap_config': 'TestAP.100001', 'topology1_snmp': 'TestAP', 'system': 'net'})
        self._await(self.ctx.netmeta_model.set_ref(cur_ref))
        olt = Olt(self.ctx, 'TestAP', 'TestAP', 'MySite', FakeClient('MyHost'), None, 'olt', None)
        olt.module = FakeModule()
        pon_port = olt.module.next_neu.data.interfaces.add()
        pon = pon_port.pons.add()
        pon.id = '100002'
        pon.name = 'GPON 100002'
        self._await(olt.load())
        # old state should be removed and new state created
        exp_ref = NetworkMetadataReference(type='ap', value='TestAP.100002', attributes={'topology1': 'GPON 100002', 'topology2': 'OLT.TestAP', 'ap_config': 'TestAP.100002', 'topology1_snmp': 'TestAP', 'system': 'net'})
        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 1)
        self.assertEqual(ap_refs.get('TestAP.100002'), exp_ref)

    def test_olt_bootstrap_with_dot_in_id(self):
        """PPA-1263 An OLT with a . in its source ID value works properly."""
        # Create existing state (that will no longer be present)
        cur_ref = NetworkMetadataReference(type='ap', value='Test.AP.100001', attributes={'topology1': 'GPON 100001', 'topology2': 'OLT.TestAP', 'ap_config': 'Test.AP.100001', 'topology1_snmp': 'Test.AP', 'system': 'net'})
        self._await(self.ctx.netmeta_model.set_ref(cur_ref))
        olt = Olt(self.ctx, 'Test.AP', 'Test.AP', 'MySite', FakeClient('MyHost'), None, 'olt', None)
        olt.module = FakeModule()
        pon_port = olt.module.next_neu.data.interfaces.add()
        pon = pon_port.pons.add()
        pon.id = '100002'
        pon.name = 'GPON 100002'
        self._await(olt.load())
        # old state should be removed and new state created
        exp_ref = NetworkMetadataReference(type='ap', value='Test.AP.100002', attributes={'topology1': 'GPON 100002', 'topology2': 'OLT.Test.AP', 'ap_config': 'Test.AP.100002', 'topology1_snmp': 'Test.AP', 'system': 'net'})
        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 1)
        self.assertEqual(ap_refs.get('Test.AP.100002'), exp_ref)

    def setup_simple_network(self, bridged_macs=True, map_bh=False, map_ds_ap=False):
        """
        Setup messages and state to represent a simple network that has enough variety
        to get a good coverage in this test suite.
        Four OLTs
         - one modular
         - one fixed
         - one with no PON interfaces populated
         - one with a PON interface with nothing connected
        One backhaul that has all the bridge tables of the second OLT, plus one.
        """
        def add_ont(msg, pon_port, pon, pon_id, link_brmacs, port_brmacs):
            """add an ont and peer to a NEU message being built."""
            nonlocal num_onts
            num_onts += 1
            ont_id = f"{num_onts:02x}"
            ont = msg.peers.add()
            ont.system_mac_address = f'00:00:02:00:00:{ont_id}'
            ont.serial_number = ont.system_mac_address.replace(':', '')
            ont_pon_port = ont.interfaces.add()
            ont_pon = ont_pon_port.pons.add()
            ont_pon.id = pon_id
            ont_pon.name = f"GPON {pon_id}"
            ont_pon.poller_hash = ont_pon.id.encode()
            olt_link = pon_port.links.add()
            olt_link.poller_hash = ont_pon_port.poller_hash
            ont_link = ont_pon_port.links.add()
            ont_link.poller_hash = pon_port.poller_hash
            olt_pon_link = pon.links.add()
            ont_pon_link = ont_pon.links.add()
            olt_pon_link.poller_hash = ont_pon.poller_hash
            ont_pon_link.poller_hash = pon.poller_hash
            for mac in link_brmacs or []:
                olt_link.bridged_mac_addresses.append(mac)
            ont_eth_port = ont.interfaces.add()
            for mac in port_brmacs or []:
                ont_eth_port.bridged_mac_addresses.append(mac)

        num_onts = 0
        self.ctx.bridge_mapping_enabled = True
        olt1 = Olt(self.ctx, 'olt1', 'Fixed OLT', 'MySite', FakeClient('ip1'), None, 'olt', None)
        olt1.module = FakeModule()
        pon_port = olt1.module.next_neu.data.interfaces.add()
        pon = pon_port.pons.add()
        pon.id = '100001'
        pon.name = f"GPON {pon.id}"
        pon.poller_hash = b'100001'
        brmacs = ['00:01:00:00:00:01'] if bridged_macs else None
        add_ont(olt1.module.next_neu, pon_port, pon, '000002000001', brmacs, None)

        olt2 = Olt(self.ctx, 'olt2', 'Modular OLT', 'MySite2', FakeClient('ip2'), None, 'olt', None)
        olt2.module = FakeModule()
        pon_card = olt2.module.next_neu.data.modules.add()
        pon_port = pon_card.interfaces.add()
        pon = pon_port.pons.add()
        pon.id = '200001'
        pon.name = f"GPON {pon.id}"
        pon.poller_hash = b'200001'
        brmacs = ['00:01:00:00:00:02', '00:01:00:00:00:03'] if bridged_macs else None
        add_ont(olt2.module.next_neu, pon_port, pon, '000003000001', None, brmacs)
        brmacs = ['00:01:00:00:00:04'] if bridged_macs else None
        add_ont(olt2.module.next_neu, pon_port, pon, '000003000002', None, brmacs)
        olt2.module.next_neu.peers[1].serial_number = None  # have one with no serno

        olt3 = Olt(self.ctx, 'olt3', 'Element with no PON', 'MySite', FakeClient('ip3'), None, 'olt', None)
        olt3.module = FakeModule()
        pon_card = olt3.module.next_neu.data.modules.add()
        pon_port = pon_card.interfaces.add()
        pon_port.id = '1'
        pon_port.poller_hash = b'olt3-1'

        olt4 = Olt(self.ctx, 'olt4', 'OLT with no ONTs', 'MySite', FakeClient('ip4'), None, 'olt', None)
        olt4.module = FakeModule()
        pon_port = olt4.module.next_neu.data.interfaces.add()
        pon_port.id = '1'
        pon_port.poller_hash = b'olt4-1'
        pon = pon_port.pons.add()
        pon.id = '400001'
        pon.name = f"GPON {pon.id}"
        pon.poller_hash = b'400001'

        self.aps = []
        if map_bh:  # add an upstream backhaul for olt2
            bh1 = Ap(self.ctx, 'bh1', 'Backhaul', 'MySite2', FakeClient('ip5'), None, 'ap', None)
            bh1.module = FakeModule()
            olt2_macs = ['00:00:01:00:00:01']  # fake mac to represent the olt
            for peer in olt2.module.next_neu.peers:
                olt2_macs.append(peer.system_mac_address)
                for interface in peer.interfaces:
                    olt2_macs.extend(interface.bridged_mac_addresses)
            bh1.module.stations = [CpeRadio('00:00:03:00:00:01', olt2_macs)]
            self.aps.append(bh1)
        if map_ds_ap:  # add a downstream AP for one of the OLT2 MACs
            ap1 = Ap(self.ctx, 'ap1', 'AP', 'MySite2', FakeClient('ip6'), None, 'ap', None)
            ap1.module = FakeModule()
            ap1.module.stations = [CpeRadio('0a:0a:0a:0a:0a:0a', ['00:01:00:00:00:03'])]
            self.aps.append(ap1)

        self.olts = [olt1, olt2, olt3, olt4]
        return olt1

    def run_poll(self):
        for ap in self.aps:
            self._await(ap.load())
        for olt in self.olts:
            self._await(olt.load())


    def test_ont_mac_mapped(self):
        """An ONT MAC is mapped (e.g. needed for router mode ONTs)"""
        self.setup_simple_network(bridged_macs=False)
        self.run_poll()

        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 3)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

    def test_ont_mac_unmapped(self):
        """An ONT MAC is unmapped when the ONT is no longer known."""
        olt = self.setup_simple_network(bridged_macs=False)
        self.run_poll()
        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 3)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

        del olt.module.next_neu.peers[0]
        del olt.module.next_neu.data.interfaces[0].links[0]
        del olt.module.next_neu.data.interfaces[0].pons[0].links[0]
        self.run_poll()

        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 2)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

    def test_new_ont(self):
        """Network metadata is setup correctly for an ONT."""
        self.setup_simple_network(bridged_macs=True)
        self.run_poll()

        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 3)
        exp_ref = NetworkMetadataReference(type='ap', value='olt1.100001', attributes={'topology1': 'GPON 100001', 'topology2': 'OLT.olt1', 'ap_config': 'olt1.100001', 'topology1_snmp': 'olt1', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt1.100001'), exp_ref)
        exp_ref = NetworkMetadataReference(type='ap', value='olt2.200001', attributes={'topology1': 'GPON 200001', 'topology2': 'OLT.olt2', 'ap_config': 'olt2.200001', 'topology1_snmp': 'olt2', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt2.200001'), exp_ref)
        exp_ref = NetworkMetadataReference(type='ap', value='olt4.400001', attributes={'topology1': 'GPON 400001', 'topology2': 'OLT.olt4', 'ap_config': 'olt4.400001', 'topology1_snmp': 'olt4', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt4.400001'), exp_ref)

        # check bridged macs
        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 7)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:01:00:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
            '00:01:00:00:00:04': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:04', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

    def test_unmap_bridged_mac(self):
        """A bridged MAC is removed from the OLT."""
        olt = self.setup_simple_network(bridged_macs=True)
        self.run_poll()

        # check bridged macs
        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 7)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:01:00:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
            '00:01:00:00:00:04': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:04', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

        del olt.module.next_neu.data.interfaces[0].links[0].bridged_mac_addresses[0]
        self._await(olt.load())

        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 6)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
            '00:01:00:00:00:04': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:04', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

    def test_unmap_macs_from_pon(self):
        """A PON is removed from an OLT.  All MACs are unmapped."""
        olt = self.setup_simple_network(bridged_macs=True)
        self.run_poll()

        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 7)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:01:00:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
            '00:01:00:00:00:04': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:04', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

        del olt.module.next_neu.data.interfaces[0].pons[0]
        del olt.module.next_neu.peers[0]
        self._await(olt.load())

        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 5)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
            '00:01:00:00:00:04': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:04', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

    def test_new_ont_with_upstream_bridge(self):
        """If bridged MACs are also found bridged through some upstream device such
           as a backhaul, they are properly mapped to the ONT."""
        self.setup_simple_network(bridged_macs=True, map_bh=True)
        self.run_poll()

        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 3)
        exp_ref = NetworkMetadataReference(type='ap', value='olt1.100001', attributes={'topology1': 'GPON 100001', 'topology2': 'OLT.olt1', 'ap_config': 'olt1.100001', 'topology1_snmp': 'olt1', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt1.100001'), exp_ref)
        exp_ref = NetworkMetadataReference(type='ap', value='olt2.200001', attributes={'topology1': 'GPON 200001', 'topology2': 'OLT.olt2', 'ap_config': 'olt2.200001', 'topology1_snmp': 'olt2', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt2.200001'), exp_ref)
        exp_ref = NetworkMetadataReference(type='ap', value='olt4.400001', attributes={'topology1': 'GPON 400001', 'topology2': 'OLT.olt4', 'ap_config': 'olt4.400001', 'topology1_snmp': 'olt4', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt4.400001'), exp_ref)

        # check bridged macs
        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 9)
        self.assertEqual(cpe_mac_refs, {
            '00:00:03:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:03:00:00:01', attributes={'ap': 'bh1', 'sm': '00:00:03:00:00:01'}),
            '00:00:01:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:01:00:00:01', attributes={'ap': 'bh1', 'sm': '00:00:03:00:00:01'}),
            '00:00:02:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:01:00:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
            '00:01:00:00:00:04': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:04', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

    def test_new_ont_with_downstream_bridge(self):
        """If bridged MACs are also found bridged through some downstream device,
           they should not be mapped to the ONT."""
        self.setup_simple_network(bridged_macs=True, map_ds_ap=True)
        self.run_poll()

        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 3)
        exp_ref = NetworkMetadataReference(type='ap', value='olt1.100001', attributes={'topology1': 'GPON 100001', 'topology2': 'OLT.olt1', 'ap_config': 'olt1.100001', 'topology1_snmp': 'olt1', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt1.100001'), exp_ref)
        exp_ref = NetworkMetadataReference(type='ap', value='olt2.200001', attributes={'topology1': 'GPON 200001', 'topology2': 'OLT.olt2', 'ap_config': 'olt2.200001', 'topology1_snmp': 'olt2', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt2.200001'), exp_ref)
        exp_ref = NetworkMetadataReference(type='ap', value='olt4.400001', attributes={'topology1': 'GPON 400001', 'topology2': 'OLT.olt4', 'ap_config': 'olt4.400001', 'topology1_snmp': 'olt4', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt4.400001'), exp_ref)

        # check bridged macs
        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 8)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:01:00:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:04': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:04', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
            '0a:0a:0a:0a:0a:0a': NetworkMetadataReference(type='cpe_mac', value='0a:0a:0a:0a:0a:0a', attributes={'ap': 'ap1', 'sm': '0a:0a:0a:0a:0a:0a'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'ap1', 'sm': '0a:0a:0a:0a:0a:0a'}),
        })

    def test_ont_with_no_system_mac(self):
        """An ONT without a known MAC address should still map bridged MACs to topology."""
        olt = self.setup_simple_network(bridged_macs=True)
        olt.module.next_neu.peers[0].system_mac_address = None
        self.run_poll()

        ap_refs = self.ctx.netmeta_model.refs.get('ap') or {}
        self.assertEqual(len(ap_refs), 3)
        exp_ref = NetworkMetadataReference(type='ap', value='olt1.100001', attributes={'topology1': 'GPON 100001', 'topology2': 'OLT.olt1', 'ap_config': 'olt1.100001', 'topology1_snmp': 'olt1', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt1.100001'), exp_ref)
        exp_ref = NetworkMetadataReference(type='ap', value='olt2.200001', attributes={'topology1': 'GPON 200001', 'topology2': 'OLT.olt2', 'ap_config': 'olt2.200001', 'topology1_snmp': 'olt2', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt2.200001'), exp_ref)
        exp_ref = NetworkMetadataReference(type='ap', value='olt4.400001', attributes={'topology1': 'GPON 400001', 'topology2': 'OLT.olt4', 'ap_config': 'olt4.400001', 'topology1_snmp': 'olt4', 'system': 'net'})
        self.assertEqual(ap_refs.get('olt4.400001'), exp_ref)

        # check bridged macs
        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 6)
        self.assertEqual(cpe_mac_refs, {
            '00:01:00:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:01', attributes={'ap': 'olt1.100001', 'serial_service': '000002000001'}),
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
            '00:01:00:00:00:04': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:04', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

    def test_bridge_mapping_disabled(self):
        """PPA-664 bridge mapping is enabled by default but can be disabled."""
        self.setup_simple_network(bridged_macs=True)
        self.ctx.bridge_mapping_enabled = None
        self.run_poll()
        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 7)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:01:00:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:01:00:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
            '00:01:00:00:00:04': NetworkMetadataReference(type='cpe_mac', value='00:01:00:00:00:04', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

        self.ctx.bridge_mapping_enabled = False
        self.run_poll()
        self.assertEqual(len(cpe_mac_refs), 3)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:01': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:01', attributes={'ap': 'olt1.100001', 'sm': '00:00:02:00:00:01', 'serial_service': '000002000001'}),
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

    def test_offline_ont(self):
        """An ONT is offline; it is sent in peers but there is no link."""
        olt = self.setup_simple_network(bridged_macs=False)
        del olt.module.next_neu.data.interfaces[0].pons[0]
        del olt.module.next_neu.peers[0].interfaces[0].pons[0]
        olt.module.next_neu.peers[0].status = network_poller_pb2.ELEMENT_STATUS_OFFLINE

        self.ctx.bridge_mapping_enabled = False
        self.run_poll()
        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 2)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })

    def test_linked_offline_ont(self):
        """PPA-1529 do not map network metadata for linked offline ONTs."""
        olt = self.setup_simple_network(bridged_macs=False)
        olt.module.next_neu.peers[0].status = network_poller_pb2.ELEMENT_STATUS_OFFLINE

        self.ctx.bridge_mapping_enabled = False
        self.run_poll()
        cpe_mac_refs = self.ctx.netmeta_model.refs.get('cpe_mac') or {}
        self.assertEqual(len(cpe_mac_refs), 2)
        self.assertEqual(cpe_mac_refs, {
            '00:00:02:00:00:02': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:02', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:02', 'serial_service': '000002000002'}),
            '00:00:02:00:00:03': NetworkMetadataReference(type='cpe_mac', value='00:00:02:00:00:03', attributes={'ap': 'olt2.200001', 'sm': '00:00:02:00:00:03'}),
        })
