import asyncio
from collections import namedtuple
import copy
from hashlib import sha256
import time
import unittest
from uuid import uuid4, UUID

from preseem import FakeNetworkMetadataModel, NetworkMetadataReference, FakePreseemModel
from preseem.model_sync import sync_preseem_elements, sync_preseem_model, ElementSyncItem
from preseem_protobuf.model.common_pb2 import ElementStatus
from preseem_protobuf.model.account_pb2 import AccountStatus
from preseem_protobuf.model.common_pb2 import AccountType
from preseem_protobuf.model.service_pb2 import SetAttachmentReq
from preseem.source_model import Attachment, AccessPoint, Olt, Router, Service

Account = namedtuple('Account', ('id', 'type', 'status', 'name', 'first_name', 'last_name'))
Package = namedtuple('Package', ('id', 'name', 'download_rate', 'upload_rate'))

class ElementRole:
    SWITCH = UUID('41f2903a-311a-448d-9d4d-86dc8ba3cb07')
    AP = UUID('ae3f1aa5-063a-4a24-9744-822a82d107d9')
    ROUTER = UUID('bf433e28-122b-41df-b49a-77ac6544deac')
    CPE_RADIO = UUID('c72115d3-daf9-43ae-9475-083d62eae74f')
    OLT = UUID('2d9027d6-8639-40eb-8b65-b86c8ee2238b')

def has_any_fields_set(message):
    for field_desc, value in message.ListFields():
        if value:
            return True
    return False


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

# XXX I came across a couple issues
# 1. same source id for different intent_roles could be possible.
# Maybe I can implicitly assume there's no overlap within a source.  Ideally.
class TestModelSync(unittest.TestCase):
    """Test the sync from nm refs to preseem model."""
    def setUp(self):
        self.loop = asyncio.new_event_loop() # needed to initialize asyncio
        #self.loop.set_debug(True)  # uncomment for more debug info
        asyncio.set_event_loop(self.loop)
        self.model = FakePreseemModel()
        self.nm_model = FakeNetworkMetadataModel(refs_by_type=True)
        self.company = self._await(self.model.Company.Create(name='test_1'))

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

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

    async def set_ap_ref(self, id, tower, sector, ip_address=None, system=None):
        ref = NetworkMetadataReference('ap', id, {'topology2': tower, 'topology1': sector})
        if ip_address:
            ref.attributes['ip_address'] = ip_address
        if system:
            ref.attributes['system'] = system
        await self.nm_model.set_ref(ref)

    def get_ref_uuid(self, ap_id):
        ref = (self.nm_model.refs.get('ap_model') or {}).get(ap_id)
        if ref:
            uuid_str = ref.attributes.get('uuid')
            if uuid_str:
                return UUID(uuid_str)
        return None

    def test_zero_aps(self):
        """Run through with both models empty."""
        async def test():
            aps = []

            uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps)

            self.assertEqual(await self.model.Site.List(company_uuid=self.company.uuid), [])
            self.assertEqual(await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid), [])
            self.assertEqual(uuid_map, {})
        self._await(test())

    def test_ap_created_elem_disc_created(self):
        """A Site and ElementDiscovery are created for an AP."""
        async def test():
            aps = [ElementSyncItem('ap01', 'AP 1', 'Site 1', '192.0.2.1')]

            uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps)

            sites = await self.model.Site.List(company_uuid=self.company.uuid)
            self.assertEqual(len(sites), 1)
            site = sites[0]
            self.assertEqual(site.name, 'Site 1')
            discs = await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid)
            self.assertEqual(len(discs), 1)
            disc = discs[0]
            self.assertEqual(disc.name, 'AP 1')
            self.assertEqual(disc.management_ip, '192.0.2.1')
            self.assertEqual(disc.inactive, False)
            self.assertEqual(disc.site_uuid, site.uuid)
            self.assertEqual(disc.source, 'test_source')
            self.assertEqual(disc.source_id, 'ap01')
            self.assertEqual(disc.element_uuid, None)
            self.assertEqual(disc.status, None)
            self.assertEqual(uuid_map, {'ap01': UUID(bytes=disc.uuid)})
        self._await(test())

    def test_ap_removed_elem_disc_inactive(self):
        """An element is removed from the billing system."""
        async def test():
            site = await self.model.Site.Create(
                    company_uuid=self.company.uuid,
                    name='Site 1')
            disc = await self.model.Element.CreateElementDiscovery(
                    company_uuid=self.company.uuid,
                    name='AP 1',
                    management_ip='192.0.2.1',
                    site_uuid=site.uuid,
                    source='test_source',
                    source_id='ap01',
                    intent_role_uuid=ElementRole.AP.bytes)

            uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [])

            sites = await self.model.Site.List(company_uuid=self.company.uuid)
            self.assertEqual(len(sites), 1)
            self.assertEqual(sites[0], site)

            discs = await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid)
            self.assertEqual(len(discs), 1)
            disc = discs[0]
            self.assertEqual(disc.name, 'AP 1')
            self.assertEqual(disc.management_ip, '192.0.2.1')
            self.assertEqual(disc.inactive, True)
            self.assertEqual(disc.site_uuid, site.uuid)
            self.assertEqual(disc.source, 'test_source')
            self.assertEqual(disc.source_id, 'ap01')
            self.assertEqual(disc.element_uuid, None)
            self.assertEqual(disc.status, None)
            self.assertEqual(uuid_map, {})
        self._await(test())

    def test_ap_modify_attributes(self):
        """The model is updated when attributes are modified."""
        async def test():
            aps = [ElementSyncItem('ap01', 'AP 1', 'Site 1', '192.0.2.1'),
                   ElementSyncItem('ap02', 'AP 2', 'Site 1', '192.0.2.2'),
                   ElementSyncItem('ap03', 'AP 3', 'Site 1', '192.0.2.3')]
            uuid_map1 = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps)
            discs = await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid)
            self.assertEqual(len(discs), 3)
            discs1 = {x.source_id: x for x in discs}
            aps = [ElementSyncItem('ap01', 'AP 01', 'Site 1', '192.0.2.1'),
                   ElementSyncItem('ap02', 'AP 2', 'Site 2', '192.0.2.2'),
                   ElementSyncItem('ap03', 'AP 3', 'Site 1', '192.0.2.30')]

            uuid_map2 = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps)

            self.assertEqual(uuid_map1, uuid_map2)
            sites = await self.model.Site.List(company_uuid=self.company.uuid)
            self.assertEqual(len(sites), 2)
            sites = {x.name: x for x in sites}
            site1 = sites.get('Site 1')
            self.assertIsNotNone(site1)
            site2 = sites.get('Site 2')
            self.assertIsNotNone(site2)
            discs = await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid)
            self.assertEqual(len(discs), 3)
            discs = {x.source_id: x for x in discs}
            self.assertGreater(discs['ap01'].updated_at, discs1['ap01'].updated_at)
            self.assertEqual(discs1['ap01'].name, 'AP 1')
            self.assertEqual(discs['ap01'].name, 'AP 01')
            discs['ap01'].name = discs1['ap01'].name
            discs['ap01'].updated_at = discs1['ap01'].updated_at
            self.assertEqual(discs1['ap01'], discs['ap01'])
            self.assertGreater(discs['ap02'].updated_at, discs1['ap02'].updated_at)
            self.assertEqual(discs1['ap02'].site_uuid, site1.uuid)
            self.assertEqual(discs['ap02'].site_uuid, site2.uuid)
            discs['ap02'].site_uuid = discs1['ap02'].site_uuid
            discs['ap02'].updated_at = discs1['ap02'].updated_at
            self.assertEqual(discs1['ap02'], discs['ap02'])
            self.assertGreater(discs['ap03'].updated_at, discs1['ap03'].updated_at)
            self.assertEqual(discs1['ap03'].management_ip, '192.0.2.3')
            self.assertEqual(discs['ap03'].management_ip, '192.0.2.30')
            discs['ap03'].management_ip = discs1['ap03'].management_ip
            discs['ap03'].updated_at = discs1['ap03'].updated_at
            self.assertEqual(discs1['ap03'], discs['ap03'])
        self._await(test())

    def test_reactivation(self):
        """APs and site objects are reused."""
        async def test():
            site = await self.model.Site.Create(
                    company_uuid=self.company.uuid,
                    name='Site 1')
            disc = await self.model.Element.CreateElementDiscovery(
                    company_uuid=self.company.uuid,
                    name='AP 1',
                    management_ip='192.0.2.1',
                    site_uuid=site.uuid,
                    source='test_source',
                    source_id='ap01',
                    intent_role_uuid=ElementRole.AP.bytes)
            await self.model.Site.Update(company_uuid=self.company.uuid, uuid=site.uuid, inactive=True)
            await self.model.Element.UpdateElementDiscovery(company_uuid=self.company.uuid, uuid=disc.uuid, inactive=True)

            aps = [ElementSyncItem('ap01', 'AP 1', 'Site 1', '192.0.2.1')]
            uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps, nm_model=self.nm_model)

            sites = await self.model.Site.List(company_uuid=self.company.uuid)
            self.assertEqual(len(sites), 1)
            self.assertEqual(sites[0].uuid, site.uuid)
            self.assertEqual(sites[0].name, site.name)
            self.assertEqual(sites[0].inactive, False)

            discs = await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid)
            self.assertEqual(len(discs), 1)
            self.assertEqual(discs[0].uuid, disc.uuid)
            self.assertEqual(discs[0].name, disc.name)
            self.assertEqual(discs[0].management_ip, disc.management_ip)
            self.assertEqual(discs[0].inactive, False)
            self.assertEqual(discs[0].site_uuid, disc.site_uuid)
            self.assertEqual(discs[0].source, 'test_source')
            self.assertEqual(discs[0].source_id, 'ap01')
            self.assertEqual(discs[0].element_uuid, None)
            self.assertEqual(discs[0].status, None)
            self.assertEqual(uuid_map, {'ap01': UUID(bytes=disc.uuid)})
        self._await(test())

    def test_resolved_ap_reactivation(self):
        """An resolved AP discovery is reactivated.  This clears the element_uuid
           attribute and the elem_discovery reference.  See STM-9823."""
        async def test():
            elem_uuid = UUID('52af5281-51aa-4869-a297-7e24fe2d18d9')
            site = await self.model.Site.Create(
                    company_uuid=self.company.uuid,
                    name='Site 1')
            disc = await self.model.Element.CreateElementDiscovery(
                    company_uuid=self.company.uuid,
                    name='AP 1',
                    management_ip='192.0.2.1',
                    site_uuid=site.uuid,
                    source='test_source',
                    source_id='ap01',
                    element_uuid=elem_uuid.bytes,
                    intent_role_uuid=ElementRole.AP.bytes)
            await self.model.Site.Update(company_uuid=self.company.uuid, uuid=site.uuid, inactive=True)
            await self.model.Element.UpdateElementDiscovery(company_uuid=self.company.uuid, uuid=disc.uuid, inactive=True)
            ref = NetworkMetadataReference('elem_discovery', str(UUID(bytes=disc.uuid)), {'element_uuid': str(elem_uuid)})
            await self.nm_model.set_ref(ref)
            self.assertEqual(len(self.nm_model.refs.get('elem_discovery', {})), 1)

            aps = [ElementSyncItem('ap01', 'AP 1', 'Site 1', '192.0.2.1')]
            uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps, nm_model=self.nm_model)

            sites = await self.model.Site.List(company_uuid=self.company.uuid)
            self.assertEqual(len(sites), 1)
            self.assertEqual(sites[0].uuid, site.uuid)
            self.assertEqual(sites[0].name, site.name)
            self.assertEqual(sites[0].inactive, False)

            discs = await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid)
            self.assertEqual(len(discs), 1)
            self.assertEqual(discs[0].uuid, disc.uuid)
            self.assertEqual(discs[0].name, disc.name)
            self.assertEqual(discs[0].management_ip, disc.management_ip)
            self.assertEqual(discs[0].inactive, False)
            self.assertEqual(discs[0].site_uuid, disc.site_uuid)
            self.assertEqual(discs[0].source, 'test_source')
            self.assertEqual(discs[0].source_id, 'ap01')
            self.assertEqual(discs[0].element_uuid, None)
            self.assertEqual(discs[0].status, None)
            self.assertEqual(uuid_map, {'ap01': UUID(bytes=disc.uuid)})
            self.assertEqual(len(self.nm_model.refs.get('elem_discovery', {})), 0)
        self._await(test())


    def test_active_site_preferred(self):
        """If multiple sites with the same name exist, an active one is used."""
        async def test():
            new_site = await self.model.Site.Create(
                    company_uuid=self.company.uuid,
                    name='Site 1')
            old_site = await self.model.Site.Create(
                    company_uuid=self.company.uuid,
                    name='Site 1')
            await self.model.Site.Update(company_uuid=self.company.uuid, uuid=old_site.uuid, inactive=True)
            aps = [ElementSyncItem('ap01', 'AP 1', 'Site 1', '192.0.2.1')]

            uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps)

            discs = await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid)
            self.assertEqual(len(discs), 1)
            disc = discs[0]
            self.assertEqual(disc.site_uuid, new_site.uuid)
        self._await(test())

    def test_empty_source_raises_error(self):
        """The source field cannot be the empty string."""
        async def test():
            aps = [ElementSyncItem('ap01', 'AP 1', 'Site 1', '192.0.2.1')]

            with self.assertRaises(ValueError):
                uuid_map = await sync_preseem_model(self.model, self.company.uuid, '', ElementRole.AP, aps)
        self._await(test())

    def test_empty_source_id_raises_error(self):
        """The source_id field cannot be the empty string."""
        async def test():
            aps = [ElementSyncItem('', 'AP 1', 'Site 1', '192.0.2.1')]

            with self.assertRaises(ValueError):
                uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps)
        self._await(test())

    def test_empty_name_raises_error(self):
        """The name field cannot be the empty string."""
        async def test():
            aps = [ElementSyncItem('ap01', '', 'Site 1', '192.0.2.1')]

            with self.assertRaises(ValueError):
                uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps)
        self._await(test())

    def test_empty_site_name_raises_error(self):
        """The site_name field cannot be the empty string."""
        async def test():
            aps = [ElementSyncItem('ap01', 'AP 1', '', '192.0.2.1')]

            with self.assertRaises(ValueError):
                uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps)
        self._await(test())

    def test_invalid_ip_address_raises_error(self):
        """The ip_address field must be a valid IP address."""
        async def test():
            aps = [ElementSyncItem('ap01', 'AP 1', 'Site 1', '192.0.2.256')]

            with self.assertRaises(ValueError):
                uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps)
        self._await(test())

    def test_duplicate_item_raises_error(self):
        """A duplicate id is not permitted."""
        async def test():
            aps = [
                    ElementSyncItem('ap01', 'AP 1', 'Site 1', '192.0.2.1'),
                    ElementSyncItem('ap01', 'AP 2', 'Site 1', '192.0.2.2')]

            with self.assertRaises(ValueError):
                uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, aps)
        self._await(test())

    def test_deactivation_only_happens_once(self):
        """Deactivation is only done once."""
        async def test():
            site = await self.model.Site.Create(
                    company_uuid=self.company.uuid,
                    name='Site 1')
            disc = await self.model.Element.CreateElementDiscovery(
                    company_uuid=self.company.uuid,
                    name='AP 1',
                    management_ip='192.0.2.1',
                    site_uuid=site.uuid,
                    source='test_source',
                    source_id='ap01',
                    intent_role_uuid=ElementRole.AP.bytes)

            # Set it to inactive once and note the inactive timestamp
            uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [])
            discs = await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid)
            self.assertEqual(len(discs), 1)
            disc = discs[0]
            self.assertEqual(disc.inactive, True)
            inactive_time = disc.inactive_time
            uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [])

            discs = await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid)
            self.assertEqual(len(discs), 1)
            disc = discs[0]
            self.assertEqual(disc.inactive, True)
            self.assertEqual(disc.inactive_time, inactive_time)
        self._await(test())

    def test_foreign_sources_are_ignored(self):
        """Deactivation is only done for elements matching this source."""
        async def test():
            site = await self.model.Site.Create(
                    company_uuid=self.company.uuid,
                    name='Site 1')
            disc = await self.model.Element.CreateElementDiscovery(
                    company_uuid=self.company.uuid,
                    name='AP 1',
                    management_ip='192.0.2.1',
                    site_uuid=site.uuid,
                    source='alt_source',
                    source_id='ap01',
                    intent_role_uuid=ElementRole.AP.bytes)

            uuid_map = await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [])

            sites = await self.model.Site.List(company_uuid=self.company.uuid)
            self.assertEqual(len(sites), 1)
            self.assertEqual(sites[0], site)

            discs = await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid)
            self.assertEqual(len(discs), 1)
            disc = discs[0]
            self.assertEqual(disc.name, 'AP 1')
            self.assertEqual(disc.management_ip, '192.0.2.1')
            self.assertEqual(disc.inactive, False)
            self.assertEqual(disc.site_uuid, site.uuid)
            self.assertEqual(disc.source, 'alt_source')
            self.assertEqual(disc.source_id, 'ap01')
            self.assertEqual(disc.element_uuid, None)
            self.assertEqual(disc.status, None)
            self.assertEqual(uuid_map, {})
        self._await(test())

    def test_package_create(self):
        """A Package is created in the model."""
        async def test():
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 1)
            pkg = packages[0]
            self.assertEqual(pkg.name, 'Package 1')
            self.assertEqual(pkg.download_rate, 25000)
            self.assertEqual(pkg.upload_rate, 5000)
            self.assertEqual(pkg.source, 'test_source')
            self.assertEqual(pkg.source_id, 'pkg01')
        self._await(test())

    def test_package_empty_source_id_raises_error(self):
        """The source_id field for a package cannot be the empty string."""
        async def test():
            pkgs = {'': Package('', 'Package 1', 25000, 5000)}
            with self.assertRaises(ValueError) as err:
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
        self._await(test())

    def test_package_empty_name_raises_error(self):
        """The name field cannot be the empty string."""
        async def test():
            pkgs = {'pkg01': Package('pkg01', '', 25000, 5000)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
        self._await(test())

    def test_package_non_integer_rate_raises_error(self):
        """The rate must be an integer if specified."""
        async def test():
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 25000, 'asdf')}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 'asdf', 5000)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
        self._await(test())

    def test_package_negative_rate_raises_error(self):
        """The rate cannot be a negative integer."""
        async def test():
            pkgs = {'pkg01': Package('pkg01', 'Package 1', -1, 5000)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 25000, -1)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
        self._await(test())

    def test_package_zero_rate_is_null(self):
        """A rate of zero is sent as a null rate."""
        async def test():
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 0, 5000),
                    'pkg02': Package('pkg02', 'Package 2', None, 0)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 2)
            packages = sorted(packages, key=lambda x: x.source_id)
            pkg = packages[0]
            self.assertEqual(pkg.name, 'Package 1')
            self.assertEqual(pkg.download_rate, None)
            self.assertEqual(pkg.upload_rate, 5000)
            self.assertEqual(pkg.source, 'test_source')
            self.assertEqual(pkg.source_id, 'pkg01')
            pkg = packages[1]
            self.assertEqual(pkg.name, 'Package 2')
            self.assertEqual(pkg.download_rate, None)
            self.assertEqual(pkg.upload_rate, None)
            self.assertEqual(pkg.source, 'test_source')
            self.assertEqual(pkg.source_id, 'pkg02')
        self._await(test())

    def test_package_update(self):
        """A Package is updated in the model."""
        async def test():
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 1)

            pkgs['pkg01'] = Package('pkg01', 'Package 1a', 50000, 10000)
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 1)
            pkg = packages[0]
            self.assertEqual(pkg.name, 'Package 1a')
            self.assertEqual(pkg.download_rate, 50000)
            self.assertEqual(pkg.upload_rate, 10000)
            self.assertEqual(pkg.source, 'test_source')
            self.assertEqual(pkg.source_id, 'pkg01')
        self._await(test())

    def test_package_reactivate(self):
        """An inactive package is reactivated in the model."""
        async def test():
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 1)
            await self.model.Package.Update(company_uuid=self.company.uuid, uuid=packages[0].uuid, inactive=True)
            pkgs['pkg01'] = Package('pkg01', 'Package 1a', 25000, 5000)
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 1)
            pkg = packages[0]
            self.assertEqual(pkg.name, 'Package 1a')
            self.assertEqual(pkg.download_rate, 25000)
            self.assertEqual(pkg.upload_rate, 5000)
            self.assertEqual(pkg.source, 'test_source')
            self.assertEqual(pkg.source_id, 'pkg01')
        self._await(test())

    def test_package_update_empty_name_raises_error(self):
        """The name field cannot be the empty string on an update."""
        async def test():
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 1)
            pkgs = {'pkg01': Package('pkg01', '', 25000, 5000)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
        self._await(test())

    def test_package_update_non_integer_rate_raises_error(self):
        """The rate must be an integer if specified on an update."""
        async def test():
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 1)
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 25000, 'asdf')}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 'asdf', 5000)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
        self._await(test())

    def test_package_update_negative_rate_raises_error(self):
        """The rate cannot be a negative integer on an update."""
        async def test():
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 1)
            pkgs = {'pkg01': Package('pkg01', 'Package 1', -1, 5000)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 25000, -1)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
        self._await(test())

    def test_package_update_zero_rate_is_null(self):
        """A rate of zero is sent as a null rate."""
        async def test():
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000),
                    'pkg02': Package('pkg02', 'Package 2', None, 5000)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 2)
            pkgs = {'pkg01': Package('pkg01', 'Package 1', 0, 5000),
                    'pkg02': Package('pkg02', 'Package 2', None, 0)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], pkgs)
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 2)
            packages = sorted(packages, key=lambda x: x.source_id)
            pkg = packages[0]
            self.assertEqual(pkg.name, 'Package 1')
            self.assertEqual(pkg.download_rate, None)
            self.assertEqual(pkg.upload_rate, 5000)
            self.assertEqual(pkg.source, 'test_source')
            self.assertEqual(pkg.source_id, 'pkg01')
            pkg = packages[1]
            self.assertEqual(pkg.name, 'Package 2')
            self.assertEqual(pkg.download_rate, None)
            self.assertEqual(pkg.upload_rate, None)
            self.assertEqual(pkg.source, 'test_source')
            self.assertEqual(pkg.source_id, 'pkg02')
        self._await(test())

    def test_package_deactivation(self):
        """Deactivation works and is only done once."""
        async def test():
            pkg = await self.model.Package.Create(
                    company_uuid=self.company.uuid,
                    source='test_source',
                    source_id='pkg01',
                    name='Package 1')

            # Set it to inactive once and note the inactive timestamp
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], {})
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 1)
            package = packages[0]
            self.assertEqual(package.inactive, True)
            inactive_time = package.inactive_time
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], {})
            packages = await self.model.Package.List(company_uuid=self.company.uuid)
            self.assertEqual(len(packages), 1)
            package = packages[0]
            self.assertEqual(package.inactive, True)
            self.assertEqual(package.inactive_time, inactive_time)
        self._await(test())

    def test_account_create(self):
        """An Account is created in the model."""
        async def test():
            accounts = {'acct01': Account('acct01', AccountType.ACCOUNT_TYPE_RESIDENTIAL, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Name01', None, None)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
            accounts = await self.model.Account.List(company_uuid=self.company.uuid)
            self.assertEqual(len(accounts), 1)
            acct = accounts[0]
            self.assertEqual(acct.type, AccountType.ACCOUNT_TYPE_RESIDENTIAL)
            self.assertEqual(acct.status, AccountStatus.ACCOUNT_STATUS_ACTIVE)
            self.assertEqual(acct.name, 'Name01')
            self.assertEqual(acct.first_name, None)
            self.assertEqual(acct.last_name, None)
        self._await(test())

    def test_account_empty_source_id_raises_error(self):
        """The source_id field for an account cannot be the empty string."""
        async def test():
            accounts = {'': Account('', AccountType.ACCOUNT_TYPE_RESIDENTIAL, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Name01', None, None)}
            with self.assertRaises(ValueError) as err:
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_type_is_optional(self):
        """The Type field is not required."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Name01', None, None)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
            accounts = await self.model.Account.List(company_uuid=self.company.uuid)
            self.assertEqual(len(accounts), 1)
            acct = accounts[0]
            self.assertEqual(acct.type, None)

    def test_account_type_wrong_type_raises_error(self):
        """The Type field must be an integer type."""
        async def test():
            accounts = {'acct01': Account('acct01', 'ACCOUNT_TYPE_RESIDENTIAL', AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Name01', None, None)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_type_bad_value_raises_error(self):
        """The Type field must be a postivite integer."""
        async def test():
            accounts = {'acct01': Account('acct01', AccountStatus.ACCOUNT_STATUS_INVALID, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Name01', None, None)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_status_is_required(self):
        """The Status field is required."""
        async def test():
            accounts = {'acct01': Account('acct01', None, None, 'Name01', None, None)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_status_wrong_type_raises_error(self):
        """The Status field must be an integer type."""
        async def test():
            accounts = {'acct01': Account('acct01', None, 'ACCOUNT_STATUS_ACTIVE', 'Name01', None, None)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_status_bad_value_raises_error(self):
        """The Status field must be a postivite integer."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_INVALID, 'Name01', None, None)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_name_is_optional(self):
        """The name field is not required."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, None, None, None)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
            accounts = await self.model.Account.List(company_uuid=self.company.uuid)
            self.assertEqual(len(accounts), 1)
            acct = accounts[0]
            self.assertEqual(acct.name, None)

    def test_account_name_wrong_type_raises_error(self):
        """The name field must be a string type."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 12334, None, None)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_name_empty_string_raises_error(self):
        """The name field must not be an empty string."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, "", None, None)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_first_name_is_optional(self):
        """The first_name field is not required."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, None, None, None)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
            accounts = await self.model.Account.List(company_uuid=self.company.uuid)
            self.assertEqual(len(accounts), 1)
            acct = accounts[0]
            self.assertEqual(acct.first_name, None)

    def test_account_first_name_wrong_type_raises_error(self):
        """The first_name field must be a string type."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, None, 12334, None)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_first_name_empty_string_raises_error(self):
        """The first_name field must not be an empty string."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, None, "", None)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_last_name_is_optional(self):
        """The last_name field is not required."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, None, None, None)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
            accounts = await self.model.Account.List(company_uuid=self.company.uuid)
            self.assertEqual(len(accounts), 1)
            acct = accounts[0]
            self.assertEqual(acct.last_name, None)

    def test_account_last_name_wrong_type_raises_error(self):
        """The last_name field must be a string type."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, None, None, 12334)}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_last_name_empty_string_raises_error(self):
        """The last_name field must not be an empty string."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, None, None, "")}
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
        self._await(test())

    def test_account_update(self):
        """An Account is updated in the model."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
            await self.model.Account.List(company_uuid=self.company.uuid)
            self.assertEqual(len(accounts), 1)

            accounts['acct01'] = Account('acct01', AccountType.ACCOUNT_TYPE_RESIDENTIAL, AccountStatus.ACCOUNT_STATUS_ACTIVE, None, 'Test', 'Account')
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
            accounts = await self.model.Account.List(company_uuid=self.company.uuid)
            self.assertEqual(len(accounts), 1)
            acct = accounts[0]
            self.assertEqual(acct.type, AccountType.ACCOUNT_TYPE_RESIDENTIAL)
            self.assertEqual(acct.status, AccountStatus.ACCOUNT_STATUS_ACTIVE)
            self.assertEqual(acct.name, None)
            self.assertEqual(acct.first_name, 'Test')
            self.assertEqual(acct.last_name, 'Account')
            self.assertTrue(acct.updated_at > acct.created_at)
        self._await(test())

    def test_account_deactivation(self):
        """Deactivation works and is only done once."""
        async def test():
            pkg = await self.model.Account.Create(
                    company_uuid=self.company.uuid,
                    source='test_source',
                    source_id='acct01',
                    status=AccountStatus.ACCOUNT_STATUS_ACTIVE)

            # Set it to inactive once and note the inactive timestamp
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts={})
            accounts = await self.model.Account.List(company_uuid=self.company.uuid)
            self.assertEqual(len(accounts), 1)
            account = accounts[0]
            self.assertEqual(account.inactive, True)
            inactive_time = account.inactive_time
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts={})
            accounts = await self.model.Account.List(company_uuid=self.company.uuid)
            self.assertEqual(len(accounts), 1)
            account = accounts[0]
            self.assertEqual(account.inactive, True)
            self.assertEqual(account.inactive_time, inactive_time)
        self._await(test())

    def test_account_reactivate(self):
        """An inactive account is reactivated in the model."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
            accounts = await self.model.Account.List(company_uuid=self.company.uuid)
            self.assertEqual(len(accounts), 1)
            await self.model.Account.Update(company_uuid=self.company.uuid, uuid=accounts[0].uuid, inactive=True)
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
            accounts = await self.model.Account.List(company_uuid=self.company.uuid)
            self.assertEqual(len(accounts), 1)
            acct = accounts[0]
            self.assertEqual(acct.inactive, False)
            self.assertEqual(acct.type, None)
            self.assertEqual(acct.status, AccountStatus.ACCOUNT_STATUS_ACTIVE)
            self.assertEqual(acct.name, 'Account 1')
            self.assertEqual(acct.source, 'test_source')
            self.assertEqual(acct.source_id, 'acct01')
        self._await(test())

    async def checkSourceService(self, ssvc):
        accounts = {}
        for account in await self.model.Account.List(company_uuid=self.company.uuid):
            if account.source_id in accounts:
                if isinstance(accounts[account.source_id], list):
                    accounts[account.source_id].append(account)
                else:
                    accounts[account.source_id] = [accounts[account.source_id], account]
            else:
                accounts[account.source_id] = account
        packages = {}
        for package in await self.model.Package.List(company_uuid=self.company.uuid):
            if package.source_id in packages:
                if isinstance(packages[package.source_id], list):
                    packages[package.source_id].append(package)
                else:
                    packages[package.source_id] = [packages[package.source_id], package]
            else:
                packages[package.source_id] = package
        services = {x.source_id: x for x in await self.model.Service.List(company_uuid=self.company.uuid)}
        msvc = services.get(ssvc.id)
        self.assertIsNotNone(msvc)
        self.assertEqual(msvc.source_id, ssvc.id)
        self.assertEqual(msvc.download_rate, ssvc.download_rate)
        self.assertEqual(msvc.upload_rate, ssvc.upload_rate)
        self.assertEqual(msvc.subscriber_identifier, ssvc.subscriber_identifier)
        self.assertEqual(msvc.source_url, ssvc.url)
        if ssvc.account_id:
            account = accounts.get(ssvc.account_id)
            if isinstance(account, list):  # multiple matching Account objects
                account = next((x for x in account if x.source==msvc.source), None)
                if account is None:  # no matching sources
                    self.assertTrue(ssvc.errors)
            if account or not ssvc.errors:
                self.assertIsNotNone(account)
                self.assertEqual(account.uuid, msvc.account_uuid)
        if ssvc.package_id:
            package = packages.get(ssvc.package_id)
            if isinstance(package, list):  # multiple matching Package objects
                package = next((x for x in package if x.source==msvc.source), None)
                if package is None:  # no matching sources
                    self.assertTrue(ssvc.errors)
            if package or not ssvc.errors:
                self.assertIsNotNone(package)
                self.assertEqual(package.uuid, msvc.package_uuid)
        if ssvc.attachment:
            if has_any_fields_set(ssvc.attachment.pb):
                self.assertEqual(ssvc.attachment.pb, msvc.attachment)
            else:
                self.assertIsNone(msvc.attachment)

    def test_service_create(self):
        """A Service is created in the model."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            packages = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            service = Service('service01', account_id='acct01', package_id='pkg01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            service.attachment.cpe_mac_address = '00:01:02:03:04:05'
            service.attachment.imsi = '310150123456789'
            service.attachment.cpe_serial_number = 'S12345'
            service.attachment.cpe_name = '123:456'
            service.attachment.device_mac_address = '00:0A:95:9D:68:FF'
            service.attachment.mac_addresses.append('00:01:02:03:04:05')
            service.attachment.network_prefixes.append('192.0.2.101')
            service.attachment.network_prefixes.append('3ffe:1900:9876::/48')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services)
            await self.checkSourceService(service)
        self._await(test())

    def test_service_no_account(self):
        """If a service references an account that does not exist, account_uuid is not set."""
        async def test():
            service = Service('service01', account_id='acct01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            service.attachment.mac_addresses.append('00:01:02:03:04:05')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services=services)
            with self.assertRaises(AssertionError):
                await self.checkSourceService(service)
            service.account_id = None
            await self.checkSourceService(service)

    def test_service_no_package(self):
        """If a service references a package that does not exist, package_uuid is not set."""
        async def test():
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            service = Service('service01', account_id='acct01', package_id='pkg01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            service.attachment.mac_addresses.append('00:01:02:03:04:05')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts)
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services=services)
            with self.assertRaises(AssertionError):
                await self.checkSourceService(service)
            service.package_id = None
            await self.checkSourceService(service)
        self._await(test())

    def test_service_account_collision_resolved(self):
        """If multiple accounts with matching source_id exist, the one with the same source as the service is matched."""
        async def test():
            acct1 = await self.model.Account.Create(
                company_uuid=self.company.uuid,
                source='test_source',
                source_id='acct01',
                status=AccountStatus.ACCOUNT_STATUS_ACTIVE)
            acct2 = await self.model.Account.Create(
                company_uuid=self.company.uuid,
                source='source2',
                source_id='acct01',
                status=AccountStatus.ACCOUNT_STATUS_ACTIVE)
            acct3 = await self.model.Account.Create(
                company_uuid=self.company.uuid,
                source='source3',
                source_id='acct01',
                status=AccountStatus.ACCOUNT_STATUS_ACTIVE)
            service = Service('service01', account_id='acct01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            service.attachment.mac_addresses.append('00:01:02:03:04:05')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services=services)
            await self.checkSourceService(service)
        self._await(test())

    def test_service_account_collision_error(self):
        """If multiple accounts with matching source_id exist and none match source, an error is flagged on the service."""
        async def test():
            acct1 = await self.model.Account.Create(
                company_uuid=self.company.uuid,
                source='source1',
                source_id='acct01',
                status=AccountStatus.ACCOUNT_STATUS_ACTIVE)
            acct2 = await self.model.Account.Create(
                company_uuid=self.company.uuid,
                source='source2',
                source_id='acct01',
                status=AccountStatus.ACCOUNT_STATUS_ACTIVE)
            acct3 = await self.model.Account.Create(
                company_uuid=self.company.uuid,
                source='source3',
                source_id='acct01',
                status=AccountStatus.ACCOUNT_STATUS_ACTIVE)
            service = Service('service01', account_id='acct01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            service.attachment.mac_addresses.append('00:01:02:03:04:05')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services=services)
            await self.checkSourceService(service)
        self._await(test())

    def test_service_package_collision_resolved(self):
        """If multiple packages with matching source_id exist, the one with the same source as the service is matched."""
        async def test():
            pkg1 = await self.model.Package.Create(
                company_uuid=self.company.uuid,
                source='test_source',
                source_id='pkg01')
            pkg2 = await self.model.Package.Create(
                company_uuid=self.company.uuid,
                source='source2',
                source_id='pkg01')
            pkg3 = await self.model.Package.Create(
                company_uuid=self.company.uuid,
                source='source3',
                source_id='pkg01')
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            service = Service('service01', account_id='acct01', package_id='pkg01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            service.attachment.mac_addresses.append('00:01:02:03:04:05')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, services=services)
            await self.checkSourceService(service)
        self._await(test())

    def test_service_package_collision_error(self):
        """If multiple packages with matching source_id exist and none match source, an error is flagged on the service."""
        async def test():
            pkg1 = await self.model.Package.Create(
                company_uuid=self.company.uuid,
                source='source1',
                source_id='pkg01')
            pkg2 = await self.model.Package.Create(
                company_uuid=self.company.uuid,
                source='source2',
                source_id='pkg01')
            pkg3 = await self.model.Package.Create(
                company_uuid=self.company.uuid,
                source='source3',
                source_id='pkg01')
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            service = Service('service01', account_id='acct01', package_id='pkg01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            service.attachment.mac_addresses.append('00:01:02:03:04:05')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, services=services)
            await self.checkSourceService(service)
        self._await(test())

    def test_service_update(self):
        """A Service is updated in the model."""
        async def test():
            # create a service
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            packages = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            service = Service('service01', account_id='acct01', package_id='pkg01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            service.attachment.cpe_mac_address = '00:01:02:03:04:05'
            service.attachment.imsi = '310150123456789'
            service.attachment.cpe_serial_number = 'S12345'
            service.attachment.cpe_name = '123:456'
            service.attachment.device_mac_address = '00:0A:95:9D:68:FF'
            service.attachment.mac_addresses.append('00:01:02:03:04:05')
            service.attachment.network_prefixes.append('192.0.2.101')
            service.attachment.network_prefixes.append('3ffe:1900:9876::/48')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services)
            await self.checkSourceService(service)

            # change something at each level
            service.upload_rate = 21000
            service.subscriber_identifier = None
            service.url = 'https://my.new.source.url'
            service.attachment.cpe_name = None
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services)
            services = {x.source_id: x for x in await self.model.Service.List(company_uuid=self.company.uuid)}
            await self.checkSourceService(service)
        self._await(test())

    def test_service_update_error(self):
        """Updating a field resolved by the syncer to an invalid value leaves the previous value."""
        async def test():
            # create a service
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            service = Service('service01', account_id='acct01', package_id=None, download_rate=100000, upload_rate=20000, subscriber_identifier='account01')
            service.attachment.cpe_mac_address = '00:01:02:03:04:05'
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, services=services)
            await self.checkSourceService(service)

            service.package_id = 'non-existent-package'
            service.download_rate = 101000
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services=services)
            with self.assertRaises(AssertionError):
                await self.checkSourceService(service)
            service.package_id = None
            await self.checkSourceService(service)
        self._await(test())

    def test_service_deactivation(self):
        """A service is deactivated if it is not passed to the syncer."""
        async def test():
            # create a service
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            service = Service('service01', account_id='acct01', package_id=None, download_rate=100000, upload_rate=20000, subscriber_identifier='account01')
            service.attachment.cpe_mac_address = '00:01:02:03:04:05'
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, services=services)
            await self.checkSourceService(service)

            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services={})
            services = {x.source_id: x for x in await self.model.Service.List(company_uuid=self.company.uuid)}
            await self.checkSourceService(service)
            msvc = services.get(service.id)
            self.assertIsNotNone(msvc)
            self.assertEqual(msvc.inactive, True)
            t = msvc.inactive_time

            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services={})
            await self.checkSourceService(service)
            services = {x.source_id: x for x in await self.model.Service.List(company_uuid=self.company.uuid)}
            msvc = services.get(service.id)
            self.assertIsNotNone(msvc)
            self.assertEqual(msvc.inactive_time, t)
        self._await(test())

    def test_service_deactivation_disabled(self):
        """Service deactivation can be disabled to leave old services created."""
        async def test():
            # create a service
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            service = Service('service01', account_id='acct01', package_id=None, download_rate=100000, upload_rate=20000, subscriber_identifier='account01')
            service.attachment.cpe_mac_address = '00:01:02:03:04:05'
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, services=services, deactivate=False)
            await self.checkSourceService(service)

            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services={}, deactivate=False)
            services = {x.source_id: x for x in await self.model.Service.List(company_uuid=self.company.uuid)}
            await self.checkSourceService(service)
            msvc = services.get(service.id)
            self.assertIsNotNone(msvc)
            self.assertEqual(msvc.inactive, False)
            self.assertEqual(msvc.inactive_time, None)
        self._await(test())

    def test_service_reactivation(self):
        """A service can be reactivated."""
        async def test():
            # create a service
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            service = Service('service01', account_id='acct01', package_id=None, download_rate=100000, upload_rate=20000, subscriber_identifier='account01')
            service.attachment.cpe_mac_address = '00:01:02:03:04:05'
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, services=services)
            await self.checkSourceService(service)

            # Deactivate it
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services={})
            await self.checkSourceService(service)
            services = {x.source_id: x for x in await self.model.Service.List(company_uuid=self.company.uuid)}
            msvc = services.get(service.id)
            self.assertIsNotNone(msvc)
            self.assertEqual(msvc.inactive, True)

            # Reactivate it
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services=services)
            await self.checkSourceService(service)
            services = {x.source_id: x for x in await self.model.Service.List(company_uuid=self.company.uuid)}
            msvc = services.get(service.id)
            self.assertIsNotNone(msvc)
            self.assertEqual(msvc.inactive, False)
        self._await(test())

    def test_service_ip_prefix_format(self):
        """If the service returns a different IP format, the comparison is done correctly."""
        async def test():
            # create a service
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            packages = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            service = Service('service01', account_id='acct01', package_id='pkg01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            service.attachment.network_prefixes.append('192.0.2.101')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services)
            await self.checkSourceService(service)

            # change the prefix that comes back to a "/32" (reflects actual behavior)
            model_services = await self.model.Service.List(company_uuid=self.company.uuid)
            msvc = model_services[0]
            self.model._services[msvc.company_uuid][msvc.uuid].attachment.network_prefixes[0] += '/32'

            # The sync shouldn't change anything in the model.
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services)
            self.assertEqual(self.model._services[msvc.company_uuid][msvc.uuid].attachment.network_prefixes[0], "192.0.2.101/32")
        self._await(test())

    def test_service_null_attachment(self):
        """If the service attachment was not set, it can be set by the sync."""
        async def test():
            # create a service
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            packages = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            service = Service('service01', account_id='acct01', package_id='pkg01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services)
            await self.checkSourceService(service)
            model_services = await self.model.Service.List(company_uuid=self.company.uuid)
            msvc = model_services[0]

            service = Service('service01', account_id='acct01', package_id='pkg01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            service.attachment.mac_addresses.append('00:01:02:03:04:05')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services)
            await self.checkSourceService(service)
        self._await(test())

    def test_service_empty_attachment(self):
        """Comparing an empty attachment is handled properly."""
        async def test():
            # create a service
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            packages = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            service = Service('service01', account_id='acct01', package_id='pkg01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services)
            await self.checkSourceService(service)
            model_services = await self.model.Service.List(company_uuid=self.company.uuid)
            msvc = model_services[0]

            service = Service('service01', account_id='acct01', package_id='pkg01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services)
            await self.checkSourceService(service)
            model_services = await self.model.Service.List(company_uuid=self.company.uuid)
            # check that we did not actually do an update
            self.assertEqual(msvc.updated_at, model_services[0].updated_at)
        self._await(test())

    def test_service_mac_order(self):
        """The order of MAC addresses doesn't matter."""
        async def test():
            # create a service
            service = Service('service01')
            services = {'service01': service}
            service.attachment.mac_addresses.append('00:01:02:03:04:05')
            service.attachment.mac_addresses.append('06:07:08:09:0a:0b')

            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services=services)
            await self.checkSourceService(service)

            model_services = await self.model.Service.List(company_uuid=self.company.uuid)
            msvc = model_services[0]

            service = Service('service01')
            services = {'service01': service}
            service.attachment.mac_addresses.append('06:07:08:09:0a:0b')
            service.attachment.mac_addresses.append('00:01:02:03:04:05')

            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], services=services)
            with self.assertRaises(AssertionError):
                await self.checkSourceService(service)  # will fail due to order
            model_services = await self.model.Service.List(company_uuid=self.company.uuid)
            # check that we did not actually do an update
            self.assertEqual(msvc.updated_at, model_services[0].updated_at)
        self._await(test())

    def test_service_update_attachment_config(self):
        """A Service can have its cpe_mac field ignored based on configuration."""
        async def test():
            # create a service
            accounts = {'acct01': Account('acct01', None, AccountStatus.ACCOUNT_STATUS_ACTIVE, 'Account 1', None, None)}
            packages = {'pkg01': Package('pkg01', 'Package 1', 25000, 5000)}
            service = Service('service01', account_id='acct01', package_id='pkg01', download_rate=100000, upload_rate=20000, subscriber_identifier='account01', url='https://my.source.url')
            service.attachment.mac_addresses.append('00:01:02:03:04:05')
            service.attachment.network_prefixes.append('192.0.2.101')
            services = {'service01': service}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services)
            await self.checkSourceService(service)
            service.attachment.pb.CopyFrom(service.attachment.pb)

            # change the CPE MAC address externally.
            svc = (await self.model.Service.List(company_uuid=self.company.uuid))[0]
            set_att_req = SetAttachmentReq()
            set_att_req.attachment.CopyFrom(svc.attachment)
            set_att_req.attachment.cpe_mac_address = "00:01:00:01:00:01"
            svc = await self.model.Service.Update(company_uuid=self.company.uuid, uuid=svc.uuid, attachment_req=set_att_req)

            # by default, it gets changed back.
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services)
            await self.checkSourceService(service)  # fails since set externally

            # change the CPE MAC address externally.
            svc = (await self.model.Service.List(company_uuid=self.company.uuid))[0]
            set_att_req = SetAttachmentReq()
            set_att_req.attachment.CopyFrom(svc.attachment)
            set_att_req.attachment.cpe_mac_address = "00:01:00:01:00:01"
            svc = await self.model.Service.Update(company_uuid=self.company.uuid, uuid=svc.uuid, attachment_req=set_att_req)

            # sync, ignoring this field so a different system is able to change it.
            # (something else has to change, too, or it won't modify the attachment.)
            service.attachment.username = "user02"
            cfg = {'service': {'attachment': {'cpe_mac_address': {'ignore': True}}}}

            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], accounts=accounts, packages=packages, services=services, cfg=cfg)
            with self.assertRaises(AssertionError):
                await self.checkSourceService(service)  # fails since set externally
        self._await(test())

    async def checkSourceElement(self, sobj, role_uuid, deleted=False):
        sites = {}
        for site in await self.model.Site.List(company_uuid=self.company.uuid):
            if site.name in sites:
                if isinstance(sites[site.name], list):
                    sites[site.name].append(site)
                else:
                    sites[site.name] = [sites[site.name], site]
            else:
                sites[site.name] = site
        mobjs = {x.source_id: x for x in await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid) if x.intent_role_uuid == role_uuid.bytes}
        mobj = mobjs.get(sobj.id)
        self.assertIsNotNone(mobj)
        self.assertEqual(mobj.source_id, sobj.id)
        self.assertEqual(mobj.name, sobj.name)
        self.assertEqual(mobj.management_ip, sobj.host)
        self.assertEqual(mobj.inactive, deleted)
        if sobj.site:
            site = sites.get(sobj.site)
            if site or not sobj.errors:
                self.assertIsNotNone(site)
                self.assertEqual(site.uuid, mobj.site_uuid)
        self.assertEqual(mobj.uuid, sobj._uuid)

    async def checkSourceRouter(self, sobj, deleted=False):
        return await self.checkSourceElement(sobj, ElementRole.ROUTER, deleted=deleted)

    async def checkSourceOlt(self, sobj, deleted=False):
        return await self.checkSourceElement(sobj, ElementRole.OLT, deleted=deleted)

    async def checkSourceAP(self, sobj, deleted=False):
        return await self.checkSourceElement(sobj, ElementRole.AP, deleted=deleted)

    def test_router_create(self):
        """An Router is created in the model."""
        async def test():
            router = Router('router01', host='192.0.2.1', name='Router 1', site='Site 1', url='https://myisp.com/routers/router01')
            routers = {'router01': router}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)
            await self.checkSourceRouter(router)
        self._await(test())

    def test_router_no_site(self):
        """If a router does not specify a site, it is still created."""
        async def test():
            router = Router('router01', host='192.0.2.1', name='Router 1')
            routers = {'router01': router}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)
            await self.checkSourceRouter(router)
        self._await(test())

    def test_router_update(self):
        """A Router is updated in the model."""
        async def test():
            # create a router
            router = Router('router01', host='192.0.2.1', name='Router 1', site='Site 1')
            routers = {'router01': router}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)
            await self.checkSourceRouter(router)

            # change attributes
            router.name = 'Router 01'
            router.host = '192.0.2.254'
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)
            await self.checkSourceRouter(router)
        self._await(test())

    def test_router_update(self):
        """A Router is updated in the model."""
        async def test():
            # create a router
            router = Router('router01', host='192.0.2.1', name='Router 1', site='Site 1')
            routers = {'router01': router}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)
            await self.checkSourceRouter(router)

            # change attributes
            router.name = 'Router 01'
            router.host = '192.0.2.254'
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)
            await self.checkSourceRouter(router)
        self._await(test())

    def test_router_update_error(self):
        """Updating a field resolved by the syncer to an invalid value leaves the previous value."""
        async def test():
            # create a router
            router = Router('router01', host='192.0.2.1', name='Router 1', site='Site 1')
            routers = {'router01': router}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)
            await self.checkSourceRouter(router)

            # test internal field checking
            router.name = ''
            self.assertEqual(router.name, 'Router 1')
            self.assertTrue(router.errors)
            router.errors.pop()
            router.host = ''
            self.assertEqual(router.host, '192.0.2.1')
            self.assertTrue(router.errors)
            router.errors.pop()

            # test field checking done by sync
            router.name = None
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)
            router.host = None
            with self.assertRaises(ValueError):
                await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)

            router.name = 'Router 1'
            router.host = '192.0.2.1'
            await self.checkSourceRouter(router)
        self._await(test())

    def test_router_uuid(self):
        """The router source object is populated with its uuid after sync."""
        async def test():
            router = Router('router01', host='192.0.2.1', name='Router 1', site='Site 1', url='https://myisp.com/routers/router01')
            routers = {'router01': router}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)
            await self.checkSourceRouter(router)
        self._await(test())

    def test_router_uuid_source(self):
        """PPA-1176 Do not set the UUID to a discovery object of another source."""
        async def test():
            router1 = Router('router01', host='192.0.2.1', name='Router 1', site='Site 1', url='https://myisp.com/routers/router01')
            router2 = Router('router01', host='192.0.2.1', name='Router 1', site='Site 1', url='https://myisp.com/routers/router01')
            routers = {'router01': router1}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)
            routers = {'router01': router2}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source_2', ElementRole.AP, [], routers=routers)
            # sync the first one again to trigger the bug
            routers = {'router01': router1}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)

            mobjs = {x.source: x for x in await self.model.Element.ListElementDiscovery(company_uuid=self.company.uuid)}
            self.assertEqual(len(mobjs), 2)
            mobj1 = mobjs.pop('test_source')
            mobj2 = mobjs.pop('test_source_2')
            self.assertIsNotNone(mobj1)
            self.assertIsNotNone(mobj2)
            self.assertEqual(mobj1.uuid, router1._uuid)
            self.assertEqual(mobj2.uuid, router2._uuid)
        self._await(test())

    def test_olt(self):
        """OLTs are handled properly in the model, the same way routers are."""
        async def test():
            # create an OLT
            olt = Olt('olt01', host='192.0.2.1', name='OLT 1', site='Site 1')
            olts = {'olt01': olt}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], olts=olts)
            await self.checkSourceOlt(olt)

            # add a router so we can test any interactions between them
            router = Router('router01', host='192.0.2.254', name='Router 1', site='Site 1')
            routers = {'router01': router}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], routers=routers)
            await self.checkSourceOlt(olt)
            await self.checkSourceRouter(router)

            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], olts=olts, routers=routers)
            await self.checkSourceOlt(olt)
            await self.checkSourceRouter(router)

            # Delete the router and update the OLT
            olt.host = '192.0.2.2'
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', ElementRole.AP, [], olts=olts, routers={})
            await self.checkSourceOlt(olt)
            await self.checkSourceRouter(router, deleted=True)
        self._await(test())

    def test_ap(self):
        """APs are handled properly the "modern" way in the model, the same way routers are."""
        async def test():
            # create an AP
            ap = AccessPoint('ap01', host='192.0.2.1', name='AP 1', site='Site 1')
            aps = {'ap01': ap}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', None, [], aps=aps)
            await self.checkSourceAP(ap)

            # add a router so we can test any interactions between them
            router = Router('router01', host='192.0.2.254', name='Router 1', site='Site 1')
            routers = {'router01': router}
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', None, [], routers=routers)
            await self.checkSourceAP(ap)
            await self.checkSourceRouter(router)

            await sync_preseem_model(self.model, self.company.uuid, 'test_source', None, [], aps=aps, routers=routers)
            await self.checkSourceAP(ap)
            await self.checkSourceRouter(router)

            # Delete the router and update the AP
            ap.host = '192.0.2.2'
            await sync_preseem_model(self.model, self.company.uuid, 'test_source', None, [], aps=aps, routers={})
            await self.checkSourceAP(ap)
            await self.checkSourceRouter(router, deleted=True)
        self._await(test())

    async def checkSourceElement2(self, sobj):
        """Check actual elements synced from source objects."""
        sites = {}
        for site in await self.model.Site.List(company_uuid=self.company.uuid):
            if site.name in sites:
                if isinstance(sites[site.name], list):
                    sites[site.name].append(site)
                else:
                    sites[site.name] = [sites[site.name], site]
            else:
                sites[site.name] = site
        mobjs = {x.source_id: x for x in await self.model.Element.List(company_uuid=self.company.uuid)}
        mobj = mobjs.get(sobj.id)
        self.assertIsNotNone(mobj)
        self.assertEqual(mobj.source_id, sobj.id)
        self.assertEqual(mobj.name, sobj.name)
        self.assertEqual(mobj.management_ip, sobj.host)
        self.assertEqual(mobj.inactive, False)
        self.assertEqual(mobj.status, ElementStatus.ELEMENT_STATUS_UNKNOWN)
        self.assertEqual(mobj.poller_hash, sobj.poller_hash)
        if sobj.site:
            site = sites.get(sobj.site)
            if site or not sobj.errors:
                self.assertIsNotNone(site)
                self.assertEqual(site.uuid, mobj.site_uuid)
        self.assertEqual(mobj.uuid, sobj._uuid)

    def test_element_sync(self):
        """Elements can be directly created and updated."""
        async def test():
            # Create two elements.
            ap_poller_hash = sha256(b"ap1").digest()
            olt_poller_hash = sha256(b"olt1").digest()
            ap = AccessPoint('ap01', host='192.0.2.1', name='AP 1', site='Site 1', poller_hash=ap_poller_hash)
            olt = Olt('olt01', name='OLT 1', site='Site 2', poller_hash=olt_poller_hash)
            elems = {'ap01': ap, 'olt01': olt}
            await sync_preseem_elements(self.model, self.company.uuid, 'test_source', elems)
            await self.checkSourceElement2(olt)
            await self.checkSourceElement2(ap)

            # Update the elements.
            ap.host = "192.0.2.2"
            olt.site = None
            await sync_preseem_elements(self.model, self.company.uuid, 'test_source', elems)
            await self.checkSourceElement2(olt)
            await self.checkSourceElement2(ap)

            mobjs = await self.model.Element.List(company_uuid=self.company.uuid)
            self.assertEqual(len(mobjs), 2)
        self._await(test())

    def test_element_sync_hardware_swap(self):
        """Hardware swaps are handled by updating the correct poller hash element."""
        async def test():
            ap_poller_hash_1 = sha256(b"ap1").digest()
            ap_poller_hash_2 = sha256(b"ap2").digest()
            olt_poller_hash = sha256(b"olt1").digest()
            ap = AccessPoint('ap01', host='192.0.2.1', name='AP 1', site='Site 1', poller_hash=ap_poller_hash_1)
            olt = Olt('olt01', name='OLT 1', site='Site 2', poller_hash=olt_poller_hash)
            elems = {'ap01': ap, 'olt01': olt}
            await sync_preseem_elements(self.model, self.company.uuid, 'test_source', elems)
            await self.checkSourceElement2(olt)
            await self.checkSourceElement2(ap)

            # The AP gets swapped for new hardware.
            ap.poller_hash = ap_poller_hash_2
            await sync_preseem_elements(self.model, self.company.uuid, 'test_source', elems)
            await self.checkSourceElement2(olt)
            await self.checkSourceElement2(ap)

            mobjs = await self.model.Element.List(company_uuid=self.company.uuid)
            self.assertEqual(len(mobjs), 3)
        self._await(test())

    def test_element_sync_source_id_change(self):
        """If the source ID changes for the same hardware, it should be the same element."""
        async def test():
            ap_poller_hash = sha256(b"ap1").digest()
            olt_poller_hash = sha256(b"olt1").digest()
            ap = AccessPoint('ap01', host='192.0.2.1', name='AP 1', site='Site 1', poller_hash=ap_poller_hash)
            olt = Olt('olt01', name='OLT 1', site='Site 2', poller_hash=olt_poller_hash)
            elems = {'ap01': ap, 'olt01': olt}
            await sync_preseem_elements(self.model, self.company.uuid, 'test_source', elems)
            await self.checkSourceElement2(olt)
            await self.checkSourceElement2(ap)

            # The AP ID get changed
            ap = AccessPoint('ap02', host='192.0.2.1', name='AP 1', site='Site 1', poller_hash=ap_poller_hash)
            elems = {'ap02': ap, 'olt01': olt}
            await sync_preseem_elements(self.model, self.company.uuid, 'test_source', elems)
            await self.checkSourceElement2(olt)
            await self.checkSourceElement2(ap)
            mobjs = await self.model.Element.List(company_uuid=self.company.uuid)
            self.assertEqual(len(mobjs), 2)

            # Change something else
            ap.name = "AP 2"
            elems = {'ap02': ap, 'olt01': olt}
            await sync_preseem_elements(self.model, self.company.uuid, 'test_source', elems)
            await self.checkSourceElement2(olt)
            await self.checkSourceElement2(ap)

            mobjs = await self.model.Element.List(company_uuid=self.company.uuid)
            self.assertEqual(len(mobjs), 2)
        self._await(test())
