import asyncio
import concurrent.futures
import queue
import threading
import time
import unittest

import preseem.grpc_nm_model
from preseem.grpc_nm_model import Op
from preseem import PreseemNetworkMetadataModel, NetworkMetadataEntity, NetworkMetadataIndex, NetworkMetadataLowercaseIndex, NetworkMetadataReference, Reference
from preseem_grpc_model import network_metadata_pb2

class _Disconnected(Exception):
    """Simulates a gRPC disconnect to stop the iterator."""
    pass

_SENTINEL = object()

class GrpcIterator:
    """Simulates a gRPC subscription iterator with a .next() method."""
    def __init__(self, messages=None):
        self._q = queue.Queue()
        for msg in (messages or []):
            self._q.put(msg)
        self._q.put(_SENTINEL)

    def put(self, item):
        self._q.put(item)

    def stop(self):
        self._q.put(_SENTINEL)

    def next(self):
        item = self._q.get()
        if item is _SENTINEL:
            raise _Disconnected
        return item


def _consume_handler(model, it):
    """Run ref_handler, collecting ops until the iterator is exhausted."""
    ops = []
    try:
        for op in model.ref_handler(it):
            ops.append(op)
    except _Disconnected:
        pass
    return ops


def _make_entity(net):
    """Helper to make a NetworkMetadataEntity in a standard way."""
    return NetworkMetadataEntity(net, time.time(), {
        'session': Reference(net),
        'snmp_session': Reference(net),
    })

class FakeMetadataClient(object):
    """Fake nm client"""
    def __init__(self):
        self.nms = []  # entities to set in the fake server
        self.refs = []  # references to set in the fake server

    def close_subscriptions(self):
        pass

    async def populate_ents(self):
        """Fake sending entities via a subscription."""
        syn = True
        async def send_msg(ent):
            """Send a message.  If ent is "None", send the LOADED action."""
            nonlocal syn
            await self.send_ent_entity_action(ent, syn=syn)
            syn = False

        for ent in self.nms:
            await send_msg(ent)
        await send_msg(None)
        self.done_fut.set_result(None)

    async def send_ent_entity_action(self, ent, delete=False, syn=False, fut=None):
        """Send a message to the subscription.  If ent is None, send the LOADED action."""
        ts = network_metadata_pb2.google_dot_protobuf_dot_timestamp__pb2.Timestamp()             
        ts.FromNanoseconds(int(time.time() * 1000000000))
        action = network_metadata_pb2.NetworkMetadataEntityAction.SET
        set_ent = None
        if ent:
            set_ent = network_metadata_pb2.NetworkMetadataEntity(address_space='default', network_prefix=ent.network_prefix, start=ts)
            preseem.grpc_nm_model._nm_set_attrs(set_ent.attributes, ent.attributes)
            if delete:
                action = network_metadata_pb2.NetworkMetadataEntityAction.DELETE
        else:
            action = network_metadata_pb2.NetworkMetadataEntityAction.LOADED
        msg = network_metadata_pb2.NetworkMetadataEntityAction(action=action, entity=set_ent, timestamp=ts)
        await self.ent_cbk(msg, syn)
        if fut:
            fut.set_result(None)

    async def send_ref_entity_action(self, ref, delete=False, syn=False, fut=None):
        """Send a ref update using the new Op-based callback signature."""
        if ref is None:
            # LOADED — not used for individual sends, only in populate_refs
            pass
        elif delete:
            await self.ref_cbk(Op.DEL, ref)
        else:
            await self.ref_cbk(Op.SET, ref)
        if fut:
            fut.set_result(None)

    def get_ref_entity_action(self, ref, delete=False):
        """Get a subscription message.  If ref is None, send the LOADED action."""
        ts = network_metadata_pb2.google_dot_protobuf_dot_timestamp__pb2.Timestamp()             
        ts.FromNanoseconds(int(time.time() * 1000000000))
        action = network_metadata_pb2.NetworkMetadataReferenceEntityAction.SET
        set_ref = None
        if ref:
            set_ref = network_metadata_pb2.NetworkMetadataReferenceEntity(type=ref.type, value=ref.value)
            preseem.grpc_nm_model._nm_set_attrs(set_ref.attributes, ref.attributes)
            if delete:
                action = network_metadata_pb2.NetworkMetadataReferenceEntityAction.DELETE
        else:
            action = network_metadata_pb2.NetworkMetadataReferenceEntityAction.LOADED
        msg = network_metadata_pb2.NetworkMetadataReferenceEntityAction(action=action, entity=set_ref, timestamp=ts)
        return msg

    async def del_nm_async(self, ent):
        """HAve an entity asynchronously deleted from the subscription."""
        loop = asyncio.get_running_loop()
        fut = loop.create_future()
        loop.create_task(self.send_ent_entity_action(ent, delete=True, fut=fut))
        await fut

    async def set_nm_async(self, ent):
        """HAve an entity asynchronously deleted from the subscription."""
        loop = asyncio.get_running_loop()
        fut = loop.create_future()
        loop.create_task(self.send_ent_entity_action(ent, fut=fut))
        await fut

    async def del_ref_async(self, ref):
        """Have a reference asynchronously deleted from the subscription."""
        loop = asyncio.get_running_loop()
        fut = loop.create_future()
        loop.create_task(self.send_ref_entity_action(ref, delete=True, fut=fut))
        await fut

    async def set_ref_async(self, ref):
        """Have a reference asynchronously set into the subscription."""
        loop = asyncio.get_running_loop()
        fut = loop.create_future()
        loop.create_task(self.send_ref_entity_action(ref, fut=fut))
        await fut

    async def populate_refs(self, handler):
        """Fake sending refs via a subscription, using the new handler/thread pattern."""
        messages = [self.get_ref_entity_action(ref) for ref in self.refs] + \
                   [self.get_ref_entity_action(None)]
        it = GrpcIterator(messages)
        loop = asyncio.get_running_loop()

        def worker():
            try:
                for op in handler(it):
                    try:
                        fut = asyncio.run_coroutine_threadsafe(self.ref_cbk(*op), loop)
                        fut.result()
                    except concurrent.futures.CancelledError:
                        return
            except _Disconnected:
                pass  # iterator exhausted

        await loop.run_in_executor(None, worker)
        if not self.done_fut.done():
            self.done_fut.set_result(None)

    async def nm_ent_subscribe(self, cbk, company_id=None):
        self.ent_cbk = cbk
        loop = asyncio.get_running_loop()
        self.done_fut = loop.create_future()
        loop.create_task(self.populate_ents())
        return self.done_fut

    async def nm_ref_subscribe(self, cbk, company_id=None, handler=None):
        self.ref_cbk = cbk
        loop = asyncio.get_running_loop()
        self.done_fut = loop.create_future()
        loop.create_task(self.populate_refs(handler))
        return self.done_fut

class TestNetworkMetadataModel(unittest.TestCase):
    def setUp(self):
        self.loop = asyncio.new_event_loop() # needed to initialize asyncio
        asyncio.set_event_loop(self.loop)
        self.client = FakeMetadataClient()
        self.model = PreseemNetworkMetadataModel(self.client, refs_by_type=True)

    def tearDown(self):
        self.model.close()
        self.loop.run_until_complete(self.loop.shutdown_asyncgens())
        self.loop.close()

    def update(self):
        return self.loop.run_until_complete(self.model.update())

    def _wait_for_queue(self):
        """Wait for the flex model queue to be emptied."""
        self.loop.run_until_complete(self.model._flex_q.join())

    def _wait(self, sec):
        self.loop.run_until_complete(asyncio.sleep(sec))

    def check_req(self, pb, flexes):
        """Verify that a list of flexes is the same as a flex request."""
        req = metrics_pb2.FlexPushRequest()
        req.flexes.extend([self.model._flex_pb(x) for x in flexes])
        self.assertEqual(pb, req)

    def del_nm(self, ent):
        self.loop.run_until_complete(self.client.del_nm_async(ent))

    def set_nm(self, ent):
        self.loop.run_until_complete(self.client.set_nm_async(ent))

    def del_ref(self, ref):
        self.loop.run_until_complete(self.client.del_ref_async(ref))

    def set_ref(self, ref):
        self.loop.run_until_complete(self.client.set_ref_async(ref))

    def test_lowercase_index(self):
        """Test lowercase reference index"""
        # Load fake refs
        self.model.close()
        indexes = [NetworkMetadataLowercaseIndex('service', 'username'),]
        self.model = PreseemNetworkMetadataModel(self.client, refs_by_type=True, indexes=indexes)
        self.client.refs = [
            NetworkMetadataReference('service', 'service1', {}),
            NetworkMetadataReference('service', 'service2', {'username': 'User1'})
        ]
        self.update()

        # Lookup from initial load
        refs = self.model.get_ref_index('service', 'username', 'user1') or {}
        self.assertEqual(len(refs), 1)
        self.assertEqual(refs.get('service2', None), self.client.refs[1])

        # Load in a new ref to update the index
        new_ref = NetworkMetadataReference('service', 'service8', {'username': 'User8'})
        self.set_ref(new_ref)
        refs = self.model.get_ref_index('service', 'username', 'user8') or {}
        self.assertEqual(len(refs), 1)
        self.assertEqual(refs.get('service8', None), new_ref)

        # Delete a ref and make sure it's removed from the index
        self.del_ref(new_ref)
        refs = self.model.get_ref_index('service', 'username', 'user8') or {}
        self.assertEqual(len(refs), 0)

    def test_nm_trie(self):
        # Load fake entities
        self.model.close()
        """Make sure network metadata entity changes update the trie index."""
        self.model = PreseemNetworkMetadataModel(self.client, refs_by_type=True)
        self.client.nms = [
            _make_entity('192.0.2.1'),
            _make_entity('192.0.2.54'),
            _make_entity('192.0.2.52/30'),
        ]
        self.update()

        # Check results of initial load
        self.assertEqual(len(self.model.nms), 3)
        self.assertEqual(len(self.model.nms), len(self.model.pt4))
        key = self.model.pt4.get_key('192.0.2.54')
        self.assertEqual(key, '192.0.2.54/32')
        par = self.model.pt4.parent(key)
        self.assertEqual(par, '192.0.2.52/30')

        # Add another entity
        self.set_nm(_make_entity('192.0.2.2'))
        self.assertEqual(len(self.model.nms), 4)
        self.assertEqual(len(self.model.nms), len(self.model.pt4))

        # Delete an entity
        self.del_nm(_make_entity('192.0.2.54'))
        self.assertEqual(len(self.model.nms), 3)
        key = self.model.pt4.get_key('192.0.2.54')
        self.assertEqual(key, '192.0.2.52/30')


def _make_ref_msg(ref, delete=False):
    """Helper to create a protobuf ref subscription message."""
    ts = network_metadata_pb2.google_dot_protobuf_dot_timestamp__pb2.Timestamp()
    ts.FromNanoseconds(int(time.time() * 1000000000))
    if ref is None:
        action = network_metadata_pb2.NetworkMetadataReferenceEntityAction.LOADED
        entity = None
    else:
        action = (network_metadata_pb2.NetworkMetadataReferenceEntityAction.DELETE
                  if delete else
                  network_metadata_pb2.NetworkMetadataReferenceEntityAction.SET)
        entity = network_metadata_pb2.NetworkMetadataReferenceEntity(
            type=ref.type, value=ref.value)
        preseem.grpc_nm_model._nm_set_attrs(entity.attributes, ref.attributes)
    return network_metadata_pb2.NetworkMetadataReferenceEntityAction(
        action=action, entity=entity, timestamp=ts)


REF_A = NetworkMetadataReference('service', 'svc1', {'username': 'Alice'})
REF_B = NetworkMetadataReference('service', 'svc2', {'username': 'Bob'})
REF_C = NetworkMetadataReference('device', 'dev1', {'hostname': 'router1'})


class TestRefHandler(unittest.TestCase):
    """Test ref_handler generator in isolation (no asyncio needed)."""

    def setUp(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)
        client = FakeMetadataClient()
        self.model = PreseemNetworkMetadataModel(client, refs_by_type=True)

    def tearDown(self):
        self.model.close()
        self.loop.close()

    def test_initial_sync_batches_refs(self):
        """During initial sync, SET messages are accumulated and yielded as a single SYN op."""
        msgs = [_make_ref_msg(REF_A), _make_ref_msg(REF_B), _make_ref_msg(None)]
        it = GrpcIterator(msgs)

        ops = _consume_handler(self.model, it)

        self.assertEqual(len(ops), 1)
        op, syn_dict = ops[0]
        self.assertEqual(op, Op.SYN)
        self.assertIn('service', syn_dict)
        self.assertEqual(len(syn_dict['service']), 2)
        self.assertEqual(syn_dict['service']['svc1'].attributes['username'], 'Alice')
        self.assertEqual(syn_dict['service']['svc2'].attributes['username'], 'Bob')

    def test_initial_sync_multiple_types(self):
        """SYN dict is keyed by type."""
        msgs = [_make_ref_msg(REF_A), _make_ref_msg(REF_C), _make_ref_msg(None)]
        it = GrpcIterator(msgs)

        ops = _consume_handler(self.model, it)

        self.assertEqual(len(ops), 1)
        _, syn_dict = ops[0]
        self.assertIn('service', syn_dict)
        self.assertIn('device', syn_dict)
        self.assertEqual(syn_dict['device']['dev1'].attributes['hostname'], 'router1')

    def test_post_sync_set_yields_immediately(self):
        """After LOADED, SET messages yield individual Op.SET ops."""
        msgs = [_make_ref_msg(REF_A), _make_ref_msg(None), _make_ref_msg(REF_B)]
        it = GrpcIterator(msgs)

        ops = _consume_handler(self.model, it)

        self.assertEqual(len(ops), 2)
        self.assertEqual(ops[0][0], Op.SYN)
        self.assertEqual(ops[1][0], Op.SET)
        self.assertEqual(ops[1][1].value, 'svc2')

    def test_post_sync_delete_yields_immediately(self):
        """After LOADED, DELETE messages yield individual Op.DEL ops."""
        msgs = [_make_ref_msg(REF_A), _make_ref_msg(None),
                _make_ref_msg(REF_A, delete=True)]
        it = GrpcIterator(msgs)

        ops = _consume_handler(self.model, it)

        self.assertEqual(len(ops), 2)
        self.assertEqual(ops[0][0], Op.SYN)
        self.assertEqual(ops[1][0], Op.DEL)
        self.assertEqual(ops[1][1].value, 'svc1')

    def test_empty_sync(self):
        """A LOADED with no preceding SETs yields an empty SYN dict."""
        it = GrpcIterator([_make_ref_msg(None)])

        ops = _consume_handler(self.model, it)

        self.assertEqual(len(ops), 1)
        self.assertEqual(ops[0][0], Op.SYN)
        self.assertEqual(ops[0][1], {})

    def test_duplicate_ref_during_sync_keeps_last(self):
        """If the same (type, value) appears twice in sync, last one wins."""
        ref_a2 = NetworkMetadataReference('service', 'svc1', {'username': 'Alice2'})
        msgs = [_make_ref_msg(REF_A), _make_ref_msg(ref_a2), _make_ref_msg(None)]
        it = GrpcIterator(msgs)

        ops = _consume_handler(self.model, it)
        syn_dict = ops[0][1]
        self.assertEqual(syn_dict['service']['svc1'].attributes['username'], 'Alice2')


class TestHandleRefUpdate2(unittest.TestCase):
    """Test handle_ref_update2 async callback in isolation."""

    def setUp(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)
        self.client = FakeMetadataClient()
        self.model = PreseemNetworkMetadataModel(self.client, refs_by_type=True)

    def tearDown(self):
        self.model.close()
        self.loop.close()

    def _run(self, coro):
        return self.loop.run_until_complete(coro)

    def test_syn_populates_refs(self):
        """Op.SYN replaces the refs dict."""
        syn_dict = {
            'service': {
                'svc1': REF_A,
                'svc2': REF_B,
            }
        }
        self._run(self.model.handle_ref_update2(Op.SYN, syn_dict))

        self.assertIs(self.model.refs, syn_dict)
        self.assertIn('service', self.model.refs_last_updated)

    def test_syn_notifies_waiters(self):
        """Op.SYN notifies the _refs_loaded condition."""
        notified = []

        async def wait_and_record():
            async with self.model._refs_loaded:
                await self.model._refs_loaded.wait()
                notified.append(True)

        async def run():
            task = self.loop.create_task(wait_and_record())
            await asyncio.sleep(0)  # let the waiter start
            await self.model.handle_ref_update2(Op.SYN, {})
            await asyncio.sleep(0)  # let the waiter finish
            return task

        task = self._run(run())
        self.assertTrue(notified)

    def test_set_adds_ref(self):
        """Op.SET adds a ref to the model."""
        self._run(self.model.handle_ref_update2(Op.SYN, {}))
        self._run(self.model.handle_ref_update2(Op.SET, REF_A))

        self.assertEqual(self.model.refs['service']['svc1'], REF_A)

    def test_set_updates_existing_ref(self):
        """Op.SET replaces an existing ref with the same key."""
        self._run(self.model.handle_ref_update2(Op.SYN, {}))
        self._run(self.model.handle_ref_update2(Op.SET, REF_A))

        updated = NetworkMetadataReference('service', 'svc1', {'username': 'Alice2'})
        self._run(self.model.handle_ref_update2(Op.SET, updated))

        self.assertEqual(self.model.refs['service']['svc1'].attributes['username'], 'Alice2')

    def test_del_removes_ref(self):
        """Op.DEL removes a ref from the model."""
        syn = {'service': {'svc1': REF_A, 'svc2': REF_B}}
        self._run(self.model.handle_ref_update2(Op.SYN, syn))
        self._run(self.model.handle_ref_update2(Op.DEL, REF_A))

        self.assertNotIn('svc1', self.model.refs.get('service', {}))
        self.assertIn('svc2', self.model.refs['service'])

    def test_del_removes_empty_type_dict(self):
        """Op.DEL cleans up the type dict when it becomes empty."""
        syn = {'service': {'svc1': REF_A}}
        self._run(self.model.handle_ref_update2(Op.SYN, syn))
        self._run(self.model.handle_ref_update2(Op.DEL, REF_A))

        self.assertNotIn('service', self.model.refs)

    def test_del_nonexistent_ref_is_noop(self):
        """Op.DEL for a ref that doesn't exist does nothing."""
        self._run(self.model.handle_ref_update2(Op.SYN, {}))
        self._run(self.model.handle_ref_update2(Op.DEL, REF_A))  # should not raise
        self.assertEqual(self.model.refs, {})

    def test_set_with_callback(self):
        """Op.SET invokes the user callback."""
        received = []

        async def cbk(ref, deleted=False):
            received.append((ref, deleted))

        self.model._cbk = cbk
        self._run(self.model.handle_ref_update2(Op.SYN, {}))
        self._run(self.model.handle_ref_update2(Op.SET, REF_A))

        self.assertEqual(len(received), 1)
        self.assertEqual(received[0], (REF_A, False))

    def test_del_with_callback(self):
        """Op.DEL invokes the user callback with deleted=True."""
        received = []

        async def cbk(ref, deleted=False):
            received.append((ref, deleted))

        self.model._cbk = cbk
        syn = {'service': {'svc1': REF_A}}
        self._run(self.model.handle_ref_update2(Op.SYN, syn))
        received.clear()  # ignore SYN callbacks
        self._run(self.model.handle_ref_update2(Op.DEL, REF_A))

        self.assertEqual(len(received), 1)
        self.assertEqual(received[0][1], True)

    def test_syn_with_callback(self):
        """Op.SYN invokes the callback for each ref."""
        received = []

        async def cbk(ref, deleted=False):
            received.append(ref)

        self.model._cbk = cbk
        syn = {'service': {'svc1': REF_A, 'svc2': REF_B}}
        self._run(self.model.handle_ref_update2(Op.SYN, syn))

        self.assertEqual(len(received), 2)
        values = {r.value for r in received}
        self.assertEqual(values, {'svc1', 'svc2'})


class TestRefHandlerWithIndex(unittest.TestCase):
    """Test that indexes are maintained through the new handler path."""

    def setUp(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)
        self.client = FakeMetadataClient()
        indexes = [NetworkMetadataLowercaseIndex('service', 'username')]
        self.model = PreseemNetworkMetadataModel(
            self.client, refs_by_type=True, indexes=indexes)

    def tearDown(self):
        self.model.close()
        self.loop.close()

    def _run(self, coro):
        return self.loop.run_until_complete(coro)

    def test_syn_builds_index(self):
        """Op.SYN rebuilds indexes from the full ref set."""
        syn = {'service': {'svc1': REF_A, 'svc2': REF_B}}
        self._run(self.model.handle_ref_update2(Op.SYN, syn))

        refs = self.model.get_ref_index('service', 'username', 'alice') or {}
        self.assertEqual(len(refs), 1)
        self.assertIn('svc1', refs)

    def test_set_updates_index(self):
        """Op.SET adds to the index."""
        self._run(self.model.handle_ref_update2(Op.SYN, {}))
        self._run(self.model.handle_ref_update2(Op.SET, REF_A))

        refs = self.model.get_ref_index('service', 'username', 'alice') or {}
        self.assertEqual(len(refs), 1)

    def test_set_update_moves_index(self):
        """Updating a ref's indexed attribute moves it in the index."""
        self._run(self.model.handle_ref_update2(Op.SYN, {}))
        self._run(self.model.handle_ref_update2(Op.SET, REF_A))

        updated = NetworkMetadataReference('service', 'svc1', {'username': 'Alicia'})
        self._run(self.model.handle_ref_update2(Op.SET, updated))

        old_refs = self.model.get_ref_index('service', 'username', 'alice') or {}
        self.assertEqual(len(old_refs), 0)

        new_refs = self.model.get_ref_index('service', 'username', 'alicia') or {}
        self.assertEqual(len(new_refs), 1)

    def test_del_removes_from_index(self):
        """Op.DEL removes from the index."""
        syn = {'service': {'svc1': REF_A}}
        self._run(self.model.handle_ref_update2(Op.SYN, syn))
        self._run(self.model.handle_ref_update2(Op.DEL, REF_A))

        refs = self.model.get_ref_index('service', 'username', 'alice') or {}
        self.assertEqual(len(refs), 0)


class TestRefIntegration(unittest.TestCase):
    """Integration test: ref_handler + handle_ref_update2 wired together via threading,
    simulating the real Subscription pattern."""

    def setUp(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self.loop)
        self.client = FakeMetadataClient()
        indexes = [NetworkMetadataLowercaseIndex('service', 'username')]
        self.model = PreseemNetworkMetadataModel(
            self.client, refs_by_type=True, indexes=indexes)

    def tearDown(self):
        self.model.close()
        self.loop.run_until_complete(self.loop.shutdown_asyncgens())
        self.loop.close()

    def _run_handler_on_messages(self, messages):
        """Run ref_handler in a thread, dispatching ops to handle_ref_update2 on the event loop."""
        it = GrpcIterator(messages)
        loop = self.loop

        def worker():
            try:
                for op in self.model.ref_handler(it):
                    fut = asyncio.run_coroutine_threadsafe(
                        self.model.handle_ref_update2(*op), loop)
                    fut.result(timeout=5)
            except _Disconnected:
                pass  # iterator exhausted, simulates gRPC disconnect

        async def run():
            await loop.run_in_executor(None, worker)

        self.loop.run_until_complete(run())

    def test_full_sync_and_live_updates(self):
        """Full flow: initial sync, then live SET and DEL."""
        messages = [
            _make_ref_msg(REF_A),
            _make_ref_msg(REF_B),
            _make_ref_msg(None),     # LOADED
            _make_ref_msg(REF_C),    # live SET
            _make_ref_msg(REF_A, delete=True),  # live DEL
        ]
        self._run_handler_on_messages(messages)

        # After sync: svc1 and svc2 loaded
        # After live: dev1 added, svc1 deleted
        self.assertNotIn('svc1', self.model.refs.get('service', {}))
        self.assertEqual(self.model.refs['service']['svc2'], REF_B)
        self.assertEqual(self.model.refs['device']['dev1'], REF_C)

        # Index should reflect final state
        refs = self.model.get_ref_index('service', 'username', 'alice') or {}
        self.assertEqual(len(refs), 0)
        refs = self.model.get_ref_index('service', 'username', 'bob') or {}
        self.assertEqual(len(refs), 1)

    def test_large_sync(self):
        """Sync with many refs to verify batching works."""
        refs = [NetworkMetadataReference('service', f'svc{i}', {'username': f'user{i}'})
                for i in range(500)]
        messages = [_make_ref_msg(r) for r in refs] + [_make_ref_msg(None)]

        self._run_handler_on_messages(messages)

        self.assertEqual(len(self.model.refs['service']), 500)

    def test_end_to_end_via_fake_client(self):
        """Full end-to-end using FakeMetadataClient.nm_ref_subscribe -> update()."""
        self.client.refs = [REF_A, REF_B, REF_C]
        self.loop.run_until_complete(self.model.update())

        self.assertEqual(len(self.model.refs['service']), 2)
        self.assertEqual(len(self.model.refs['device']), 1)
        refs = self.model.get_ref_index('service', 'username', 'bob') or {}
        self.assertEqual(len(refs), 1)
