#
#
#

from __future__ import absolute_import, division, print_function, \
    unicode_literals

from dyn.tm.errors import DynectGetError
from dyn.tm.services.dsf import DSFResponsePool
from json import loads
from mock import MagicMock, call, patch
from unittest import TestCase

from octodns.record import Create, Delete, Record, Update
from octodns.provider.base import Plan
from octodns.provider.dyn import DynProvider, _CachingDynZone, DSFMonitor, \
    _dynamic_value_sort_key
from octodns.zone import Zone

from helpers import SimpleProvider


class _DummyPool(object):

    def __init__(self, response_pool_id):
        self.response_pool_id = response_pool_id
        self.deleted = False

    def delete(self):
        self.deleted = True


class TestDynProvider(TestCase):
    expected = Zone('unit.tests.', [])
    for name, data in (
        ('', {
            'type': 'A',
            'ttl': 300,
            'values': ['1.2.3.4']
        }),
        ('cname', {
            'type': 'CNAME',
            'ttl': 301,
            'value': 'unit.tests.'
        }),
        ('', {
            'type': 'MX',
            'ttl': 302,
            'values': [{
                'preference': 10,
                'exchange': 'smtp-1.unit.tests.'
            }, {
                'preference': 20,
                'exchange': 'smtp-2.unit.tests.'
            }]
        }),
        ('naptr', {
            'type': 'NAPTR',
            'ttl': 303,
            'values': [{
                'order': 100,
                'preference': 101,
                'flags': 'U',
                'service': 'SIP+D2U',
                'regexp': '!^.*$!sip:info@foo.example.com!',
                'replacement': '.',
            }, {
                'order': 200,
                'preference': 201,
                'flags': 'U',
                'service': 'SIP+D2U',
                'regexp': '!^.*$!sip:info@bar.example.com!',
                'replacement': '.',
            }]
        }),
        ('sub', {
            'type': 'NS',
            'ttl': 3600,
            'values': ['ns3.p10.dynect.net.', 'ns3.p10.dynect.net.'],
        }),
        ('ptr', {
            'type': 'PTR',
            'ttl': 304,
            'value': 'xx.unit.tests.'
        }),
        ('spf', {
            'type': 'SPF',
            'ttl': 305,
            'values': ['v=spf1 ip4:192.168.0.1/16-all', 'v=spf1 -all'],
        }),
        ('', {
            'type': 'SSHFP',
            'ttl': 306,
            'value': {
                'algorithm': 1,
                'fingerprint_type': 1,
                'fingerprint': 'bf6b6825d2977c511a475bbefb88aad54a92ac73',
            }
        }),
        ('_srv._tcp', {
            'type': 'SRV',
            'ttl': 307,
            'values': [{
                'priority': 11,
                'weight': 12,
                'port': 10,
                'target': 'foo-1.unit.tests.'
            }, {
                'priority': 21,
                'weight': 22,
                'port': 20,
                'target': 'foo-2.unit.tests.'
            }]}),
        ('', {
            'type': 'CAA',
            'ttl': 308,
            'values': [{
                'flags': 0,
                'tag': 'issue',
                'value': 'ca.unit.tests'
            }]})):
        expected.add_record(Record.new(expected, name, data))

    @classmethod
    def setUpClass(self):
        # Get the DynectSession creation out of the way so that tests can
        # ignore it
        with patch('dyn.core.SessionEngine.execute',
                   return_value={'status': 'success'}):
            provider = DynProvider('test', 'cust', 'user', 'pass')
            provider._check_dyn_sess()

    def setUp(self):
        # Flush our zone to ensure we start fresh
        _CachingDynZone.flush_zone(self.expected.name[:-1])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_non_existent(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Test Zone create
        execute_mock.side_effect = [
            DynectGetError('foo'),
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        execute_mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
        ])
        self.assertEquals(set(), got.records)

    @patch('dyn.core.SessionEngine.execute')
    def test_populate(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Test Zone create
        execute_mock.side_effect = [
            # get Zone
            {'data': {}},
            # get_all_records
            {'data': {
                'a_records': [{
                    'fqdn': 'unit.tests',
                    'rdata': {'address': '1.2.3.4'},
                    'record_id': 1,
                    'record_type': 'A',
                    'ttl': 300,
                    'zone': 'unit.tests',
                }],
                'cname_records': [{
                    'fqdn': 'cname.unit.tests',
                    'rdata': {'cname': 'unit.tests.'},
                    'record_id': 2,
                    'record_type': 'CNAME',
                    'ttl': 301,
                    'zone': 'unit.tests',
                }],
                'ns_records': [{
                    'fqdn': 'unit.tests',
                    'rdata': {'nsdname': 'ns1.p10.dynect.net.'},
                    'record_id': 254597562,
                    'record_type': 'NS',
                    'service_class': '',
                    'ttl': 3600,
                    'zone': 'unit.tests'
                }, {
                    'fqdn': 'unit.tests',
                    'rdata': {'nsdname': 'ns2.p10.dynect.net.'},
                    'record_id': 254597563,
                    'record_type': 'NS',
                    'service_class': '',
                    'ttl': 3600,
                    'zone': 'unit.tests'
                }, {
                    'fqdn': 'unit.tests',
                    'rdata': {'nsdname': 'ns3.p10.dynect.net.'},
                    'record_id': 254597564,
                    'record_type': 'NS',
                    'service_class': '',
                    'ttl': 3600,
                    'zone': 'unit.tests'
                }, {
                    'fqdn': 'unit.tests',
                    'rdata': {'nsdname': 'ns4.p10.dynect.net.'},
                    'record_id': 254597565,
                    'record_type': 'NS',
                    'service_class': '',
                    'ttl': 3600,
                    'zone': 'unit.tests'
                }, {
                    'fqdn': 'sub.unit.tests',
                    'rdata': {'nsdname': 'ns3.p10.dynect.net.'},
                    'record_id': 254597564,
                    'record_type': 'NS',
                    'service_class': '',
                    'ttl': 3600,
                    'zone': 'unit.tests'
                }, {
                    'fqdn': 'sub.unit.tests',
                    'rdata': {'nsdname': 'ns3.p10.dynect.net.'},
                    'record_id': 254597564,
                    'record_type': 'NS',
                    'service_class': '',
                    'ttl': 3600,
                    'zone': 'unit.tests'
                }],
                'mx_records': [{
                    'fqdn': 'unit.tests',
                    'rdata': {'exchange': 'smtp-1.unit.tests.',
                              'preference': 10},
                    'record_id': 3,
                    'record_type': 'MX',
                    'ttl': 302,
                    'zone': 'unit.tests',
                }, {
                    'fqdn': 'unit.tests',
                    'rdata': {'exchange': 'smtp-2.unit.tests.',
                              'preference': 20},
                    'record_id': 4,
                    'record_type': 'MX',
                    'ttl': 302,
                    'zone': 'unit.tests',
                }],
                'naptr_records': [{
                    'fqdn': 'naptr.unit.tests',
                    'rdata': {'flags': 'U',
                              'order': 100,
                              'preference': 101,
                              'regexp': '!^.*$!sip:info@foo.example.com!',
                              'replacement': '.',
                              'services': 'SIP+D2U'},
                    'record_id': 5,
                    'record_type': 'MX',
                    'ttl': 303,
                    'zone': 'unit.tests',
                }, {
                    'fqdn': 'naptr.unit.tests',
                    'rdata': {'flags': 'U',
                              'order': 200,
                              'preference': 201,
                              'regexp': '!^.*$!sip:info@bar.example.com!',
                              'replacement': '.',
                              'services': 'SIP+D2U'},
                    'record_id': 6,
                    'record_type': 'MX',
                    'ttl': 303,
                    'zone': 'unit.tests',
                }],
                'ptr_records': [{
                    'fqdn': 'ptr.unit.tests',
                    'rdata': {'ptrdname': 'xx.unit.tests.'},
                    'record_id': 7,
                    'record_type': 'PTR',
                    'ttl': 304,
                    'zone': 'unit.tests',
                }],
                'soa_records': [{
                    'fqdn': 'unit.tests',
                    'rdata': {'txtdata': 'ns1.p16.dynect.net. '
                              'hostmaster.unit.tests. 4 3600 600 604800 1800'},
                    'record_id': 99,
                    'record_type': 'SOA',
                    'ttl': 299,
                    'zone': 'unit.tests',
                }],
                'spf_records': [{
                    'fqdn': 'spf.unit.tests',
                    'rdata': {'txtdata': 'v=spf1 ip4:192.168.0.1/16-all'},
                    'record_id': 8,
                    'record_type': 'SPF',
                    'ttl': 305,
                    'zone': 'unit.tests',
                }, {
                    'fqdn': 'spf.unit.tests',
                    'rdata': {'txtdata': 'v=spf1 -all'},
                    'record_id': 8,
                    'record_type': 'SPF',
                    'ttl': 305,
                    'zone': 'unit.tests',
                }],
                'sshfp_records': [{
                    'fqdn': 'unit.tests',
                    'rdata': {'algorithm': 1,
                              'fingerprint':
                              'bf6b6825d2977c511a475bbefb88aad54a92ac73',
                              'fptype': 1},
                    'record_id': 9,
                    'record_type': 'SSHFP',
                    'ttl': 306,
                    'zone': 'unit.tests',
                }],
                'srv_records': [{
                    'fqdn': '_srv._tcp.unit.tests',
                    'rdata': {'port': 10,
                              'priority': 11,
                              'target': 'foo-1.unit.tests.',
                              'weight': 12},
                    'record_id': 10,
                    'record_type': 'SRV',
                    'ttl': 307,
                    'zone': 'unit.tests',
                }, {
                    'fqdn': '_srv._tcp.unit.tests',
                    'rdata': {'port': 20,
                              'priority': 21,
                              'target': 'foo-2.unit.tests.',
                              'weight': 22},
                    'record_id': 11,
                    'record_type': 'SRV',
                    'ttl': 307,
                    'zone': 'unit.tests',
                }],
                'caa_records': [{
                    'fqdn': 'unit.tests',
                    'rdata': {'flags': 0,
                              'tag': 'issue',
                              'value': 'ca.unit.tests'},
                    'record_id': 12,
                    'record_type': 'cAA',
                    'ttl': 308,
                    'zone': 'unit.tests',
                }],
            }}
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        execute_mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])
        changes = self.expected.changes(got, SimpleProvider())
        self.assertEquals([], changes)

    @patch('dyn.core.SessionEngine.execute')
    def test_sync(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Test Zone create
        execute_mock.side_effect = [
            # No such zone, during populate
            DynectGetError('foo'),
            # No such zone, during sync
            DynectGetError('foo'),
            # get empty Zone
            {'data': {}},
            # get zone we can modify & delete with
            {'data': {
                # A top-level to delete
                'a_records': [{
                    'fqdn': 'unit.tests',
                    'rdata': {'address': '1.2.3.4'},
                    'record_id': 1,
                    'record_type': 'A',
                    'ttl': 30,
                    'zone': 'unit.tests',
                }, {
                    'fqdn': 'a.unit.tests',
                    'rdata': {'address': '2.3.4.5'},
                    'record_id': 2,
                    'record_type': 'A',
                    'ttl': 30,
                    'zone': 'unit.tests',
                }],
                # A node to delete
                'cname_records': [{
                    'fqdn': 'cname.unit.tests',
                    'rdata': {'cname': 'unit.tests.'},
                    'record_id': 3,
                    'record_type': 'CNAME',
                    'ttl': 30,
                    'zone': 'unit.tests',
                }],
                # A record to leave alone
                'ptr_records': [{
                    'fqdn': 'ptr.unit.tests',
                    'rdata': {'ptrdname': 'xx.unit.tests.'},
                    'record_id': 4,
                    'record_type': 'PTR',
                    'ttl': 30,
                    'zone': 'unit.tests',
                }],
                # A record to modify
                'srv_records': [{
                    'fqdn': '_srv._tcp.unit.tests',
                    'rdata': {'port': 10,
                              'priority': 11,
                              'target': 'foo-1.unit.tests.',
                              'weight': 12},
                    'record_id': 5,
                    'record_type': 'SRV',
                    'ttl': 30,
                    'zone': 'unit.tests',
                }, {
                    'fqdn': '_srv._tcp.unit.tests',
                    'rdata': {'port': 20,
                              'priority': 21,
                              'target': 'foo-2.unit.tests.',
                              'weight': 22},
                    'record_id': 6,
                    'record_type': 'SRV',
                    'ttl': 30,
                    'zone': 'unit.tests',
                }],
            }}
        ]

        # No existing records, create all
        with patch('dyn.tm.zones.Zone.add_record') as add_mock:
            with patch('dyn.tm.zones.Zone._update') as update_mock:
                plan = provider.plan(self.expected)
                update_mock.assert_not_called()
                provider.apply(plan)
                update_mock.assert_called()
                self.assertFalse(plan.exists)
            add_mock.assert_called()
            # Once for each dyn record (8 Records, 2 of which have dual values)
            self.assertEquals(15, len(add_mock.call_args_list))
        execute_mock.assert_has_calls([call('/Zone/unit.tests/', 'GET', {}),
                                       call('/Zone/unit.tests/', 'GET', {})])
        self.assertEquals(10, len(plan.changes))

        execute_mock.reset_mock()

        # Delete one and modify another
        new = Zone('unit.tests.', [])
        for name, data in (
            ('a', {
                'type': 'A',
                'ttl': 30,
                'value': '2.3.4.5'
            }),
            ('ptr', {
                'type': 'PTR',
                'ttl': 30,
                'value': 'xx.unit.tests.'
            }),
            ('_srv._tcp', {
                'type': 'SRV',
                'ttl': 30,
                'values': [{
                    'priority': 31,
                    'weight': 12,
                    'port': 10,
                    'target': 'foo-1.unit.tests.'
                }, {
                    'priority': 21,
                    'weight': 22,
                    'port': 20,
                    'target': 'foo-2.unit.tests.'
                }]})):
            new.add_record(Record.new(new, name, data))

        with patch('dyn.tm.zones.Zone.add_record') as add_mock:
            with patch('dyn.tm.records.DNSRecord.delete') as delete_mock:
                with patch('dyn.tm.zones.Zone._update') as update_mock:
                    plan = provider.plan(new)
                    provider.apply(plan)
                    update_mock.assert_called()
                    self.assertTrue(plan.exists)
                # we expect 4 deletes, 2 from actual deletes and 2 from
                # updates which delete and recreate
                self.assertEquals(4, len(delete_mock.call_args_list))
            # the 2 (re)creates
            self.assertEquals(2, len(add_mock.call_args_list))
        execute_mock.assert_has_calls([
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])
        self.assertEquals(3, len(plan.changes))


class TestDynProviderGeo(TestCase):

    with open('./tests/fixtures/dyn-traffic-director-get.json') as fh:
        traffic_director_response = loads(fh.read())

    @property
    def traffic_directors_response(self):
        return {
            'data': [{
                'active': 'Y',
                'label': 'unit.tests.:A',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }, {
                'active': 'Y',
                'label': 'some.other.:A',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }, {
                'active': 'Y',
                'label': 'other format',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }]
        }

    # Doing this as a property so that we get a fresh copy each time, dyn's
    # client lib messes with the return value and prevents it from working on
    # subsequent uses otherwise
    @property
    def records_response(self):
        return {
            'data': {
                'a_records': [{
                    'fqdn': 'unit.tests',
                    'rdata': {'address': '1.2.3.4'},
                    'record_id': 1,
                    'record_type': 'A',
                    'ttl': 301,
                    'zone': 'unit.tests',
                }],
            }
        }

    monitor_id = '42a'
    monitors_response = {
        'data': [{
            'active': 'Y',
            'agent_scheme': 'geo',
            'dsf_monitor_id': monitor_id,
            'endpoints': [],
            'label': 'unit.tests.:A',
            'notifier': [],
            'expected': '',
            'header': 'User-Agent: Dyn Monitor',
            'host': 'unit.tests',
            'path': '/_dns',
            'port': '443',
            'timeout': '10',
            'probe_interval': '60',
            'protocol': 'HTTPS',
            'response_count': '2',
            'retries': '2',
            'services': ['12311']
        }, {
            'active': 'Y',
            'agent_scheme': 'geo',
            'dsf_monitor_id': 'b52',
            'endpoints': [],
            'label': 'old-label.unit.tests.',
            'notifier': [],
            'expected': '',
            'header': 'User-Agent: Dyn Monitor',
            'host': 'old-label.unit.tests',
            'path': '/_dns',
            'port': '443',
            'timeout': '10',
            'probe_interval': '60',
            'protocol': 'HTTPS',
            'response_count': '2',
            'retries': '2',
            'services': ['12312']
        }],
        'job_id': 3376281406,
        'msgs': [{
            'ERR_CD': None,
            'INFO': 'DSFMonitor_get: Here are your monitors',
            'LVL': 'INFO',
            'SOURCE': 'BLL'
        }],
        'status': 'success'
    }

    expected_geo = Zone('unit.tests.', [])
    geo_record = Record.new(expected_geo, '', {
        'geo': {
            'AF': ['2.2.3.4', '2.2.3.5'],
            'AS-JP': ['3.2.3.4', '3.2.3.5'],
            'NA-US': ['4.2.3.4', '4.2.3.5'],
            'NA-US-CA': ['5.2.3.4', '5.2.3.5']
        },
        'ttl': 300,
        'type': 'A',
        'values': ['1.2.3.4', '1.2.3.5'],
    })
    expected_geo.add_record(geo_record)
    expected_regular = Zone('unit.tests.', [])
    regular_record = Record.new(expected_regular, '', {
        'ttl': 301,
        'type': 'A',
        'value': '1.2.3.4',
    })
    expected_regular.add_record(regular_record)

    def setUp(self):
        # Flush our zone to ensure we start fresh
        _CachingDynZone.flush_zone('unit.tests')

    @patch('dyn.core.SessionEngine.execute')
    def test_traffic_directors(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass', True)
        # short-circuit session checking
        provider._dyn_sess = True
        provider.log.warn = MagicMock()

        # no tds
        mock.side_effect = [{'data': []}]
        self.assertEquals({}, provider.traffic_directors)

        # a supported td and an ignored one
        response = {
            'data': [{
                'active': 'Y',
                'label': 'unit.tests.:A',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }, {
                'active': 'Y',
                'label': 'geo.unit.tests.:A',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }, {
                'active': 'Y',
                'label': 'something else',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }],
            'job_id': 3376164583,
            'status': 'success'
        }
        mock.side_effect = [response]
        # first make sure that we get the empty version from cache
        self.assertEquals({}, provider.traffic_directors)
        # reach in and bust the cache
        provider._traffic_directors = None
        tds = provider.traffic_directors
        self.assertEquals(set(['unit.tests.', 'geo.unit.tests.']),
                          set(tds.keys()))
        self.assertEquals(['A'], tds['unit.tests.'].keys())
        self.assertEquals(['A'], tds['geo.unit.tests.'].keys())
        provider.log.warn.assert_called_with("Unsupported TrafficDirector "
                                             "'%s'", 'something else')

    @patch('dyn.core.SessionEngine.execute')
    def test_traffic_director_monitor(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass', True)
        # short-circuit session checking
        provider._dyn_sess = True
        existing = Zone('unit.tests.', [])

        # no monitors, will try and create
        geo_monitor_id = '42x'
        mock.side_effect = [self.monitors_response, {
            'data': {
                'active': 'Y',
                'dsf_monitor_id': geo_monitor_id,
                'endpoints': [],
                'label': 'geo.unit.tests.:A',
                'notifier': '',
                'expected': '',
                'header': 'User-Agent: Dyn Monitor',
                'host': 'geo.unit.tests.',
                'path': '/_dns',
                'port': '443',
                'timeout': '10',
                'probe_interval': '60',
                'protocol': 'HTTPS',
                'response_count': '2',
                'retries': '2'
            },
            'job_id': 3376259461,
            'msgs': [{'ERR_CD': None,
                      'INFO': 'add: Here is the new monitor',
                      'LVL': 'INFO',
                      'SOURCE': 'BLL'}],
            'status': 'success'
        }]

        # ask for a monitor that doesn't exist
        record = Record.new(existing, 'geo', {
            'ttl': 60,
            'type': 'A',
            'value': '1.2.3.4',
            'octodns': {
                'healthcheck': {
                    'host': 'foo.bar',
                    'path': '/_ready'
                }
            }
        })
        monitor = provider._traffic_director_monitor(record)
        self.assertEquals(geo_monitor_id, monitor.dsf_monitor_id)
        # should see a request for the list and a create
        mock.assert_has_calls([
            call('/DSFMonitor/', 'GET', {'detail': 'Y'}),
            call('/DSFMonitor/', 'POST', {
                'retries': 2,
                'protocol': 'HTTPS',
                'response_count': 2,
                'label': 'geo.unit.tests.:A',
                'probe_interval': 60,
                'active': 'Y',
                'options': {
                    'path': '/_ready',
                    'host': 'foo.bar',
                    'header': 'User-Agent: Dyn Monitor',
                    'port': 443,
                    'timeout': 10
                }
            })
        ])
        # created monitor is now cached
        self.assertTrue('geo.unit.tests.:A' in
                        provider._traffic_director_monitors)
        # pre-existing one is there too
        self.assertTrue('unit.tests.:A' in
                        provider._traffic_director_monitors)

        # now ask for a monitor that does exist
        record = Record.new(existing, '', {
            'ttl': 60,
            'type': 'A',
            'value': '1.2.3.4'
        })
        mock.reset_mock()
        monitor = provider._traffic_director_monitor(record)
        self.assertEquals(self.monitor_id, monitor.dsf_monitor_id)
        # should have resulted in no calls b/c exists & we've cached the list
        mock.assert_not_called()

        # and finally for a monitor that exists, but with a differing config
        record = Record.new(existing, '', {
            'octodns': {
                'healthcheck': {
                    'host': 'bleep.bloop',
                    'path': '/_nope',
                    'protocol': 'HTTP',
                    'port': 8080,
                }
            },
            'ttl': 60,
            'type': 'A',
            'value': '1.2.3.4'
        })
        mock.reset_mock()
        mock.side_effect = [{
            'data': {
                'active': 'Y',
                'dsf_monitor_id': self.monitor_id,
                'endpoints': [],
                'label': 'unit.tests.:A',
                'notifier': '',
                'expected': '',
                'header': 'User-Agent: Dyn Monitor',
                'host': 'bleep.bloop',
                'path': '/_nope',
                'port': '8080',
                'timeout': '10',
                'probe_interval': '60',
                'protocol': 'HTTP',
                'response_count': '2',
                'retries': '2'
            },
            'job_id': 3376259461,
            'msgs': [{'ERR_CD': None,
                      'INFO': 'add: Here is the new monitor',
                      'LVL': 'INFO',
                      'SOURCE': 'BLL'}],
            'status': 'success'
        }]
        monitor = provider._traffic_director_monitor(record)
        self.assertEquals(self.monitor_id, monitor.dsf_monitor_id)
        # should have resulted an update
        mock.assert_has_calls([
            call('/DSFMonitor/42a/', 'PUT', {
                'protocol': 'HTTP',
                'options': {
                    'path': '/_nope',
                    'host': 'bleep.bloop',
                    'header': 'User-Agent: Dyn Monitor',
                    'port': 8080,
                    'timeout': 10
                }
            })
        ])
        # cached monitor should have been updated
        self.assertTrue('unit.tests.:A' in
                        provider._traffic_director_monitors)
        monitor = provider._traffic_director_monitors['unit.tests.:A']
        self.assertEquals('bleep.bloop', monitor.host)
        self.assertEquals('/_nope', monitor.path)
        self.assertEquals('HTTP', monitor.protocol)
        self.assertEquals('8080', monitor.port)

        # test upgrading an old label
        record = Record.new(existing, 'old-label', {
            'ttl': 60,
            'type': 'A',
            'value': '1.2.3.4'
        })
        mock.reset_mock()
        mock.side_effect = [{
            'data': {
                'active': 'Y',
                'dsf_monitor_id': self.monitor_id,
                'endpoints': [],
                'label': 'old-label.unit.tests.:A',
                'notifier': '',
                'expected': '',
                'header': 'User-Agent: Dyn Monitor',
                'host': 'old-label.unit.tests',
                'path': '/_dns',
                'port': '443',
                'timeout': '10',
                'probe_interval': '60',
                'protocol': 'HTTPS',
                'response_count': '2',
                'retries': '2'
            },
            'job_id': 3376259461,
            'msgs': [{'ERR_CD': None,
                      'INFO': 'add: Here is the new monitor',
                      'LVL': 'INFO',
                      'SOURCE': 'BLL'}],
            'status': 'success'
        }]
        monitor = provider._traffic_director_monitor(record)
        self.assertEquals(self.monitor_id, monitor.dsf_monitor_id)
        # should have resulted an update
        mock.assert_has_calls([
            call('/DSFMonitor/b52/', 'PUT', {
                'label': 'old-label.unit.tests.:A'
            })
        ])
        # cached monitor should have been updated
        self.assertTrue('old-label.unit.tests.:A' in
                        provider._traffic_director_monitors)

    @patch('dyn.core.SessionEngine.execute')
    def test_extra_changes(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass', True)
        # short-circuit session checking
        provider._dyn_sess = True

        mock.side_effect = [self.monitors_response]

        # non-geo
        desired = Zone('unit.tests.', [])
        record = Record.new(desired, '', {
            'ttl': 60,
            'type': 'A',
            'value': '1.2.3.4',
        })
        desired.add_record(record)
        extra = provider._extra_changes(desired=desired,
                                        changes=[Create(record)])
        self.assertEquals(0, len(extra))

        # in changes, noop
        desired = Zone('unit.tests.', [])
        record = Record.new(desired, '', {
            'geo': {
                'NA': ['1.2.3.4'],
            },
            'ttl': 60,
            'type': 'A',
            'value': '1.2.3.4',
        })
        desired.add_record(record)
        extra = provider._extra_changes(desired=desired,
                                        changes=[Create(record)])
        self.assertEquals(0, len(extra))

        # no diff, no extra
        extra = provider._extra_changes(desired=desired, changes=[])
        self.assertEquals(0, len(extra))

        # monitors should have been fetched now
        mock.assert_called_once()

        # diff in healthcheck, gets extra
        desired = Zone('unit.tests.', [])
        record = Record.new(desired, '', {
            'geo': {
                'NA': ['1.2.3.4'],
            },
            'octodns': {
                'healthcheck': {
                    'host': 'foo.bar',
                    'path': '/_ready'
                }
            },
            'ttl': 60,
            'type': 'A',
            'value': '1.2.3.4',
        })
        desired.add_record(record)
        extra = provider._extra_changes(desired=desired, changes=[])
        self.assertEquals(1, len(extra))
        extra = extra[0]
        self.assertIsInstance(extra, Update)
        self.assertEquals(record, extra.record)

        # missing health check
        desired = Zone('unit.tests.', [])
        record = Record.new(desired, 'geo', {
            'geo': {
                'NA': ['1.2.3.4'],
            },
            'ttl': 60,
            'type': 'A',
            'value': '1.2.3.4',
        })
        desired.add_record(record)
        extra = provider._extra_changes(desired=desired, changes=[])
        self.assertEquals(1, len(extra))
        extra = extra[0]
        self.assertIsInstance(extra, Update)
        self.assertEquals(record, extra.record)

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_traffic_directors_empty(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # empty all around
        mock.side_effect = [
            # get traffic directors
            {'data': []},
            # get zone
            {'data': {}},
            # get records
            {'data': {}},
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        self.assertEquals(0, len(got.records))
        mock.assert_has_calls([
            call('/DSF/', 'GET', {'detail': 'Y'}),
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}),
        ])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_traffic_directors_td(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        got = Zone('unit.tests.', [])
        zone_name = got.name[:-1]
        # only traffic director
        mock.side_effect = [
            # get traffic directors
            self.traffic_directors_response,
            # get the first td's nodes
            {'data': [{'fqdn': zone_name, 'zone': zone_name}]},
            # get traffic director, b/c ^ matches
            self.traffic_director_response,
            # get the next td's nodes, not a match
            {'data': [{'fqdn': 'other', 'zone': 'other'}]},
            # get zone
            {'data': {}},
            # get records
            {'data': {}},
        ]
        provider.populate(got)
        self.assertEquals(1, len(got.records))
        self.assertFalse(self.expected_geo.changes(got, provider))
        mock.assert_has_calls([
            call('/DSF/', 'GET', {'detail': 'Y'}),
            call('/DSFNode/2ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
            call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
                 {'pending_changes': 'Y'}),
            call('/DSFNode/3ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_traffic_directors_regular(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # only regular
        mock.side_effect = [
            # get traffic directors
            {'data': []},
            # get zone
            {'data': {}},
            # get records
            self.records_response
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        self.assertEquals(1, len(got.records))
        self.assertFalse(self.expected_regular.changes(got, provider))
        mock.assert_has_calls([
            call('/DSF/', 'GET', {'detail': 'Y'}),
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'}),
        ])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_traffic_directors_both(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # both traffic director and regular, regular is ignored
        mock.side_effect = [
            # get traffic directors
            self.traffic_directors_response,
            # grab its nodes, matches
            {'data': [{'fqdn': 'unit.tests', 'zone': 'unit.tests'}]},
            # get traffic director b/c match
            self.traffic_director_response,
            # grab next td's nodes, not a match
            {'data': [{'fqdn': 'other', 'zone': 'other'}]},
            # get zone
            {'data': {}},
            # get records
            self.records_response
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        self.assertEquals(1, len(got.records))
        self.assertFalse(self.expected_geo.changes(got, provider))
        mock.assert_has_calls([
            call('/DSF/', 'GET', {'detail': 'Y'}),
            call('/DSFNode/2ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
            call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
                 {'pending_changes': 'Y'}),
            call('/DSFNode/3ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate_traffic_director_busted(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        busted_traffic_director_response = {
            "status": "success",
            "data": {
                "notifiers": [],
                "rulesets": [],
                "ttl": "300",
                "active": "Y",
                "service_id": "oIRZ4lM-W64NUelJGuzuVziZ4MI",
                "nodes": [{
                    "fqdn": "unit.tests",
                    "zone": "unit.tests"
                }],
                "pending_change": "",
                "label": "unit.tests.:A"
            },
            "job_id": 3376642606,
            "msgs": [{
                "INFO": "detail: Here is your service",
                "LVL": "INFO",
                "ERR_CD": None,
                "SOURCE": "BLL"
            }]
        }
        # busted traffic director
        mock.side_effect = [
            # get traffic directors
            self.traffic_directors_response,
            {'data': [{'fqdn': 'unit.tests', 'zone': 'unit.tests'}]},
            # get traffic director
            busted_traffic_director_response,
            {'data': [{'fqdn': 'other', 'zone': 'other'}]},
            # get zone
            {'data': {}},
            # get records
            {'data': {}},
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        self.assertEquals(1, len(got.records))
        # we expect a change here for the record, the values aren't important,
        # so just compare set contents (which does name and type)
        self.assertEquals(self.expected_geo.records, got.records)
        mock.assert_has_calls([
            call('/DSF/', 'GET', {'detail': 'Y'}),
            call('/DSFNode/2ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
            call('/DSF/2ERWXQNsb_IKG2YZgYqkPvk0PBM/', 'GET',
                 {'pending_changes': 'Y'}),
            call('/DSFNode/3ERWXQNsb_IKG2YZgYqkPvk0PBM', 'GET', {}),
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])

    @patch('dyn.core.SessionEngine.execute')
    def test_apply_traffic_director(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # stubbing these out to avoid a lot of messy mocking, they'll be tested
        # individually, we'll check for expected calls
        provider._mod_geo_Create = MagicMock()
        provider._mod_geo_Update = MagicMock()
        provider._mod_geo_Delete = MagicMock()
        provider._mod_Create = MagicMock()
        provider._mod_Update = MagicMock()
        provider._mod_Delete = MagicMock()

        # busted traffic director
        mock.side_effect = [
            # get zone
            {'data': {}},
            # accept publish
            {'data': {}},
        ]
        desired = Zone('unit.tests.', [])
        geo = self.geo_record
        regular = self.regular_record

        changes = [
            Create(geo),
            Create(regular),
            Update(geo, geo),
            Update(regular, regular),
            Delete(geo),
            Delete(regular),
        ]
        plan = Plan(None, desired, changes, True)
        provider._apply(plan)
        mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
            call('/Zone/unit.tests/', 'PUT', {'publish': True})
        ])
        # should have seen 1 call to each
        provider._mod_geo_Create.assert_called_once()
        provider._mod_geo_Update.assert_called_once()
        provider._mod_geo_Delete.assert_called_once()
        provider._mod_Create.assert_called_once()
        provider._mod_Update.assert_called_once()
        provider._mod_Delete.assert_called_once()

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_geo_create(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # will be tested separately
        provider._mod_geo_rulesets = MagicMock()

        mock.side_effect = [
            # create traffic director
            self.traffic_director_response,
            # get traffic directors
            self.traffic_directors_response
        ]
        provider._mod_geo_Create(None, Create(self.geo_record))
        # td now lives in cache
        self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
        # should have seen 1 gen call
        provider._mod_geo_rulesets.assert_called_once()

    def test_mod_geo_update_geo_geo(self):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # update of an existing td

        # pre-populate the cache with our mock td
        provider._traffic_directors = {
            'unit.tests.': {
                'A': 42,
            }
        }
        # mock _mod_geo_rulesets
        provider._mod_geo_rulesets = MagicMock()

        geo = self.geo_record
        change = Update(geo, geo)
        provider._mod_geo_Update(None, change)
        # still in cache
        self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
        # should have seen 1 gen call
        provider._mod_geo_rulesets.assert_called_once_with(42, change)

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_geo_update_geo_regular(self, _):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # convert a td to a regular record

        provider._mod_Create = MagicMock()
        provider._mod_geo_Delete = MagicMock()

        change = Update(self.geo_record, self.regular_record)
        provider._mod_geo_Update(42, change)
        # should have seen a call to create the new regular record
        provider._mod_Create.assert_called_once_with(42, change)
        # should have seen a call to delete the old td record
        provider._mod_geo_Delete.assert_called_once_with(42, change)

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_geo_update_regular_geo(self, _):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # convert a regular record to a td

        provider._mod_geo_Create = MagicMock()
        provider._mod_Delete = MagicMock()

        change = Update(self.regular_record, self.geo_record)
        provider._mod_geo_Update(42, change)
        # should have seen a call to create the new geo record
        provider._mod_geo_Create.assert_called_once_with(42, change)
        # should have seen a call to delete the old regular record
        provider._mod_Delete.assert_called_once_with(42, change)

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_geo_delete(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        td_mock = MagicMock()
        provider._traffic_directors = {
            'unit.tests.': {
                'A': td_mock,
            }
        }
        provider._mod_geo_Delete(None, Delete(self.geo_record))
        # delete called
        td_mock.delete.assert_called_once()
        # removed from cache
        self.assertFalse('A' in provider.traffic_directors['unit.tests.'])

    @patch('dyn.tm.services.DSFResponsePool.create')
    def test_find_or_create_geo_pool(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        td = 42

        # no candidates cache miss, so create
        values = ['1.2.3.4', '1.2.3.5']
        pool = provider._find_or_create_geo_pool(td, [], 'default', 'A',
                                                 values)
        self.assertIsInstance(pool, DSFResponsePool)
        self.assertEquals(1, len(pool.rs_chains))
        records = pool.rs_chains[0].record_sets[0].records
        self.assertEquals(values, [r.address for r in records])
        mock.assert_called_once_with(td)

        # cache hit, use the one we just created
        mock.reset_mock()
        pools = [pool]
        cached = provider._find_or_create_geo_pool(td, pools, 'default', 'A',
                                                   values)
        self.assertEquals(pool, cached)
        mock.assert_not_called()

        # cache miss, non-matching label
        mock.reset_mock()
        miss = provider._find_or_create_geo_pool(td, pools, 'NA-US-CA', 'A',
                                                 values)
        self.assertNotEquals(pool, miss)
        self.assertEquals('NA-US-CA', miss.label)
        mock.assert_called_once_with(td)

        # cache miss, matching label, mis-matching values
        mock.reset_mock()
        values = ['2.2.3.4.', '2.2.3.5']
        miss = provider._find_or_create_geo_pool(td, pools, 'default', 'A',
                                                 values)
        self.assertNotEquals(pool, miss)
        mock.assert_called_once_with(td)

    @patch('dyn.tm.services.DSFRuleset.add_response_pool')
    @patch('dyn.tm.services.DSFRuleset.create')
    # just lets us ignore the pool.create calls
    @patch('dyn.tm.services.DSFResponsePool.create')
    def test_mod_geo_rulesets_create(self, _, ruleset_create_mock,
                                     add_response_pool_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        td_mock = MagicMock()
        td_mock._rulesets = []
        provider._traffic_director_monitor = MagicMock()
        provider._find_or_create_geo_pool = MagicMock()

        td_mock.all_response_pools = []

        provider._find_or_create_geo_pool.side_effect = [
            _DummyPool('default'),
            _DummyPool(1),
            _DummyPool(2),
            _DummyPool(3),
            _DummyPool(4),
        ]

        change = Create(self.geo_record)
        provider._mod_geo_rulesets(td_mock, change)
        ruleset_create_mock.assert_has_calls((
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
        ))
        add_response_pool_mock.assert_has_calls((
            # default
            call('default'),
            # first geo and it's fallback
            call(1),
            call('default', index=999),
            # 2nd geo and it's fallback
            call(2),
            call('default', index=999),
            # 3nd geo and it's fallback
            call(3),
            call('default', index=999),
            # 4th geo and it's 2 levels of fallback
            call(4),
            call(3, index=999),
            call('default', index=999),
        ))

    # have to patch the place it's imported into, not where it lives
    @patch('octodns.provider.dyn.get_response_pool')
    @patch('dyn.tm.services.DSFRuleset.add_response_pool')
    @patch('dyn.tm.services.DSFRuleset.create')
    # just lets us ignore the pool.create calls
    @patch('dyn.tm.services.DSFResponsePool.create')
    def test_mod_geo_rulesets_existing(self, _, ruleset_create_mock,
                                       add_response_pool_mock,
                                       get_response_pool_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        ruleset_mock = MagicMock()
        ruleset_mock.response_pools = [_DummyPool(3)]

        td_mock = MagicMock()
        td_mock._rulesets = [
            ruleset_mock,
        ]
        provider._traffic_director_monitor = MagicMock()
        provider._find_or_create_geo_pool = MagicMock()

        unused_pool = _DummyPool('unused')
        td_mock.all_response_pools = \
            ruleset_mock.response_pools + [unused_pool]
        get_response_pool_mock.return_value = unused_pool

        provider._find_or_create_geo_pool.side_effect = [
            _DummyPool('default'),
            _DummyPool(1),
            _DummyPool(2),
            ruleset_mock.response_pools[0],
            _DummyPool(4),
        ]

        change = Create(self.geo_record)
        provider._mod_geo_rulesets(td_mock, change)
        ruleset_create_mock.assert_has_calls((
            call(td_mock, index=2),
            call(td_mock, index=2),
            call(td_mock, index=2),
            call(td_mock, index=2),
            call(td_mock, index=2),
        ))
        add_response_pool_mock.assert_has_calls((
            # default
            call('default'),
            # first geo and it's fallback
            call(1),
            call('default', index=999),
            # 2nd geo and it's fallback
            call(2),
            call('default', index=999),
            # 3nd geo, from existing, and it's fallback
            call(3),
            call('default', index=999),
            # 4th geo and it's 2 levels of fallback
            call(4),
            call(3, index=999),
            call('default', index=999),
        ))
        # unused poll should have been deleted
        self.assertTrue(unused_pool.deleted)
        # old ruleset ruleset should be deleted, it's pool will have been
        # reused
        ruleset_mock.delete.assert_called_once()


class TestDynProviderAlias(TestCase):
    expected = Zone('unit.tests.', [])
    for name, data in (
            ('', {
                'type': 'ALIAS',
                'ttl': 300,
                'value': 'www.unit.tests.'
            }),
            ('www', {
                'type': 'A',
                'ttl': 300,
                'values': ['1.2.3.4']
            })):
        expected.add_record(Record.new(expected, name, data))

    def setUp(self):
        # Flush our zone to ensure we start fresh
        _CachingDynZone.flush_zone(self.expected.name[:-1])

    @patch('dyn.core.SessionEngine.execute')
    def test_populate(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Test Zone create
        execute_mock.side_effect = [
            # get Zone
            {'data': {}},
            # get_all_records
            {'data': {
                'a_records': [{
                    'fqdn': 'www.unit.tests',
                    'rdata': {'address': '1.2.3.4'},
                    'record_id': 1,
                    'record_type': 'A',
                    'ttl': 300,
                    'zone': 'unit.tests',
                }],
                'alias_records': [{
                    'fqdn': 'unit.tests',
                    'rdata': {'alias': 'www.unit.tests.'},
                    'record_id': 2,
                    'record_type': 'ALIAS',
                    'ttl': 300,
                    'zone': 'unit.tests',
                }],
            }}
        ]
        got = Zone('unit.tests.', [])
        provider.populate(got)
        execute_mock.assert_has_calls([
            call('/Zone/unit.tests/', 'GET', {}),
            call('/AllRecord/unit.tests/unit.tests./', 'GET', {'detail': 'Y'})
        ])
        changes = self.expected.changes(got, SimpleProvider())
        self.assertEquals([], changes)

    @patch('dyn.core.SessionEngine.execute')
    def test_sync(self, execute_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Test Zone create
        execute_mock.side_effect = [
            # No such zone, during populate
            DynectGetError('foo'),
            # No such zone, during sync
            DynectGetError('foo'),
            # get empty Zone
            {'data': {}},
            # get zone we can modify & delete with
            {'data': {
                # A top-level to delete
                'a_records': [{
                    'fqdn': 'www.unit.tests',
                    'rdata': {'address': '1.2.3.4'},
                    'record_id': 1,
                    'record_type': 'A',
                    'ttl': 300,
                    'zone': 'unit.tests',
                }],
                # A node to delete
                'alias_records': [{
                    'fqdn': 'unit.tests',
                    'rdata': {'alias': 'www.unit.tests.'},
                    'record_id': 2,
                    'record_type': 'ALIAS',
                    'ttl': 300,
                    'zone': 'unit.tests',
                }],
            }}
        ]

        # No existing records, create all
        with patch('dyn.tm.zones.Zone.add_record') as add_mock:
            with patch('dyn.tm.zones.Zone._update') as update_mock:
                plan = provider.plan(self.expected)
                update_mock.assert_not_called()
                provider.apply(plan)
                update_mock.assert_called()
            add_mock.assert_called()
            # Once for each dyn record
            self.assertEquals(2, len(add_mock.call_args_list))
        execute_mock.assert_has_calls([call('/Zone/unit.tests/', 'GET', {}),
                                       call('/Zone/unit.tests/', 'GET', {})])
        self.assertEquals(2, len(plan.changes))


# Need a class that doesn't do all the "real" stuff, but gets our monkey
# patching
class DummyDSFMonitor(DSFMonitor):

    def __init__(self, host=None, path=None, protocol=None, port=None,
                 options_host=None, options_path=None, options_protocol=None,
                 options_port=None):
        # not calling super on purpose
        self._host = host
        self._path = path
        self._protocol = protocol
        self._port = port
        if options_host:
            self._options = {
                'host': options_host,
                'path': options_path,
                'protocol': options_protocol,
                'port': options_port,
            }
        else:
            self._options = None


class TestDSFMonitorMonkeyPatching(TestCase):

    def test_host(self):
        monitor = DummyDSFMonitor(host='host.com', path='/path',
                                  protocol='HTTP', port=8080)
        self.assertEquals('host.com', monitor.host)
        self.assertEquals('/path', monitor.path)
        self.assertEquals('HTTP', monitor.protocol)
        self.assertEquals(8080, monitor.port)

        monitor = DummyDSFMonitor(options_host='host.com',
                                  options_path='/path',
                                  options_protocol='HTTP', options_port=8080)
        self.assertEquals('host.com', monitor.host)
        self.assertEquals('/path', monitor.path)

        monitor.host = 'other.com'
        self.assertEquals('other.com', monitor.host)
        monitor.path = '/other-path'
        self.assertEquals('/other-path', monitor.path)
        monitor.protocol = 'HTTPS'
        self.assertEquals('HTTPS', monitor.protocol)
        monitor.port = 8081
        self.assertEquals(8081, monitor.port)

        monitor = DummyDSFMonitor()
        monitor.host = 'other.com'
        self.assertEquals('other.com', monitor.host)
        monitor = DummyDSFMonitor()
        monitor.path = '/other-path'
        self.assertEquals('/other-path', monitor.path)
        monitor.protocol = 'HTTP'
        self.assertEquals('HTTP', monitor.protocol)
        monitor.port = 8080
        self.assertEquals(8080, monitor.port)

        # Just to exercise the _options init
        monitor = DummyDSFMonitor()
        monitor.protocol = 'HTTP'
        self.assertEquals('HTTP', monitor.protocol)
        monitor = DummyDSFMonitor()
        monitor.port = 8080
        self.assertEquals(8080, monitor.port)


class DummyRecord(object):

    def __init__(self, address, weight, ttl):
        self.address = address
        self.weight = weight
        self.ttl = ttl


class DummyRecordSets(object):

    def __init__(self, records):
        self.records = records


class DummyRsChains(object):

    def __init__(self, records):
        self.record_sets = [DummyRecordSets(records)]


class DummyResponsePool(object):

    def __init__(self, label, records=[]):
        self.label = label
        if records:
            self.rs_chains = [DummyRsChains(records)]
        else:
            self.rs_chains = []

    def refresh(self):
        pass


class DummyRuleset(object):

    def __init__(self, label, response_pools=[],
                 criteria_type='always', criteria={}):
        self.label = label
        self.response_pools = response_pools
        self.criteria_type = criteria_type
        self.criteria = criteria


class DummyTrafficDirector(object):

    def __init__(self, zone_name, rulesets=[], response_pools=[], ttl=42):
        self.label = 'dummy:abcdef1234567890'
        self.rulesets = rulesets
        self.all_response_pools = response_pools
        self.ttl = ttl
        self.nodes = [{'zone': zone_name[:-1]}]


class TestDynProviderDynamic(TestCase):

    def test_value_for_address(self):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        class DummyRecord(object):

            def __init__(self, address, weight):
                self.address = address
                self.weight = weight

        record = DummyRecord('1.2.3.4', 32)
        self.assertEquals({
            'value': record.address,
            'weight': record.weight,
        }, provider._value_for_A('A', record))

        record = DummyRecord('2601:644:500:e210:62f8:1dff:feb8:947a', 32)
        self.assertEquals({
            'value': record.address,
            'weight': record.weight,
        }, provider._value_for_AAAA('AAAA', record))

    def test_value_for_CNAME(self):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        class DummyRecord(object):

            def __init__(self, cname, weight):
                self.cname = cname
                self.weight = weight

        record = DummyRecord('foo.unit.tests.', 32)
        self.assertEquals({
            'value': record.cname,
            'weight': record.weight,
        }, provider._value_for_CNAME('CNAME', record))

    def test_populate_dynamic_pools(self):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Empty data, empty returns
        default, pools = provider._populate_dynamic_pools('A', [], [])
        self.assertEquals({}, default)
        self.assertEquals({}, pools)

        records_a = [DummyRecord('1.2.3.4', 32, 60)]
        default_a = DummyResponsePool('default', records_a)

        # Just a default A
        response_pools = [default_a]
        default, pools = provider._populate_dynamic_pools('A', [],
                                                          response_pools)
        self.assertEquals({
            'ttl': 60,
            'type': 'A',
            'values': ['1.2.3.4'],
        }, default)
        self.assertEquals({}, pools)

        multi_a = [
            DummyRecord('1.2.3.5', 42, 90),
            DummyRecord('1.2.3.6', 43, 90),
            DummyRecord('1.2.3.7', 44, 90),
        ]
        example_a = DummyResponsePool('example', multi_a)

        # Just a named pool
        response_pools = [example_a]
        default, pools = provider._populate_dynamic_pools('A', [],
                                                          response_pools)
        self.assertEquals({}, default)
        self.assertEquals({
            'example': {
                'values': [{
                    'value': '1.2.3.5',
                    'weight': 42,
                }, {
                    'value': '1.2.3.6',
                    'weight': 43,
                }, {
                    'value': '1.2.3.7',
                    'weight': 44,
                }],
            },
        }, pools)

        # Named pool that shows up twice
        response_pools = [example_a, example_a]
        default, pools = provider._populate_dynamic_pools('A', [],
                                                          response_pools)
        self.assertEquals({}, default)
        self.assertEquals({
            'example': {
                'values': [{
                    'value': '1.2.3.5',
                    'weight': 42,
                }, {
                    'value': '1.2.3.6',
                    'weight': 43,
                }, {
                    'value': '1.2.3.7',
                    'weight': 44,
                }],
            },
        }, pools)

        # Default & named
        response_pools = [example_a, default_a, example_a]
        default, pools = provider._populate_dynamic_pools('A', [],
                                                          response_pools)
        self.assertEquals({
            'ttl': 60,
            'type': 'A',
            'values': ['1.2.3.4'],
        }, default)
        self.assertEquals({
            'example': {
                'values': [{
                    'value': '1.2.3.5',
                    'weight': 42,
                }, {
                    'value': '1.2.3.6',
                    'weight': 43,
                }, {
                    'value': '1.2.3.7',
                    'weight': 44,
                }],
            },
        }, pools)

        # empty rs_chains doesn't cause an example, just ignores
        empty_a = DummyResponsePool('empty')
        response_pools = [empty_a]
        default, pools = provider._populate_dynamic_pools('A', [],
                                                          response_pools)
        self.assertEquals({}, default)
        self.assertEquals({}, pools)

    def test_populate_dynamic_rules(self):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Empty
        rulesets = []
        pools = {}
        rules = provider._populate_dynamic_rules(rulesets, pools)
        self.assertEquals([], rules)

        # default: is ignored
        rulesets = [DummyRuleset('default:')]
        pools = {}
        rules = provider._populate_dynamic_rules(rulesets, pools)
        self.assertEquals([], rules)

        # No ResponsePools in RuleSet, ignored
        rulesets = [DummyRuleset('0:abcdefg')]
        pools = {}
        rules = provider._populate_dynamic_rules(rulesets, pools)
        self.assertEquals([], rules)

        # ResponsePool, no fallback
        rulesets = [DummyRuleset('0:abcdefg', [
            DummyResponsePool('some-pool')
        ])]
        pools = {}
        rules = provider._populate_dynamic_rules(rulesets, pools)
        self.assertEquals([{
            'pool': 'some-pool',
        }], rules)

        # ResponsePool, with dfault fallback (ignored)
        rulesets = [DummyRuleset('0:abcdefg', [
            DummyResponsePool('some-pool'),
            DummyResponsePool('default'),
        ])]
        pools = {}
        rules = provider._populate_dynamic_rules(rulesets, pools)
        self.assertEquals([{
            'pool': 'some-pool',
        }], rules)

        # ResponsePool, with fallback
        rulesets = [DummyRuleset('0:abcdefg', [
            DummyResponsePool('some-pool'),
            DummyResponsePool('some-fallback'),
        ])]
        pools = {
            'some-pool': {},
        }
        rules = provider._populate_dynamic_rules(rulesets, pools)
        self.assertEquals([{
            'pool': 'some-pool',
        }], rules)
        # fallback has been installed
        self.assertEquals({
            'some-pool': {
                'fallback': 'some-fallback',
            }
        }, pools)

        # Unsupported criteria_type (ignored)
        rulesets = [DummyRuleset('0:abcdefg', [
            DummyResponsePool('some-pool')
        ], 'unsupported')]
        pools = {}
        rules = provider._populate_dynamic_rules(rulesets, pools)
        self.assertEquals([], rules)

        # Geo Continent/Region
        response_pools = [DummyResponsePool('some-pool')]
        criteria = {
            'geoip': {
                'country': ['US'],
                'province': ['or'],
                'region': [14],
            },
        }
        ruleset = DummyRuleset('0:abcdefg', response_pools,
                               'geoip', criteria)
        rulesets = [ruleset]
        pools = {}
        rules = provider._populate_dynamic_rules(rulesets, pools)
        self.assertEquals([{
            'geos': ['AF', 'NA-US', 'NA-US-OR'],
            'pool': 'some-pool',
        }], rules)

    def test_populate_dynamic_traffic_director(self):
        provider = DynProvider('test', 'cust', 'user', 'pass')
        fqdn = 'dynamic.unit.tests.'

        multi_a = [
            DummyRecord('1.2.3.5', 1, 90),
            DummyRecord('1.2.3.6', 1, 90),
            DummyRecord('1.2.3.7', 1, 90),
        ]
        default_response_pool = DummyResponsePool('default', multi_a)
        pool1_response_pool = DummyResponsePool('pool1', multi_a)
        rulesets = [
            DummyRuleset('default', [default_response_pool]),
            DummyRuleset('0:abcdef', [pool1_response_pool], 'geoip', {
                'geoip': {
                    'country': ['US'],
                    'province': ['or'],
                    'region': [14],
                },
            }),
        ]
        zone = Zone('unit.tests.', [])
        td = DummyTrafficDirector(zone.name, rulesets,
                                  [default_response_pool, pool1_response_pool])
        record = provider._populate_dynamic_traffic_director(zone, fqdn, 'A',
                                                             td, rulesets,
                                                             True)
        self.assertTrue(record)
        self.assertEquals('A', record._type)
        self.assertEquals(90, record.ttl)
        self.assertEquals([
            '1.2.3.5',
            '1.2.3.6',
            '1.2.3.7',
        ], record.values)
        self.assertTrue('pool1' in record.dynamic.pools)
        self.assertEquals({
            'fallback': None,
            'values': [{
                'value': '1.2.3.5',
                'weight': 1,
            }, {
                'value': '1.2.3.6',
                'weight': 1,
            }, {
                'value': '1.2.3.7',
                'weight': 1,
            }]
        }, record.dynamic.pools['pool1'].data)
        self.assertEquals(2, len(record.dynamic.rules))
        self.assertEquals({
            'pool': 'default',
        }, record.dynamic.rules[0].data)
        self.assertEquals({
            'pool': 'pool1',
            'geos': ['AF', 'NA-US', 'NA-US-OR'],
        }, record.dynamic.rules[1].data)

        # Hack into the provider and create a fake list of traffic directors
        provider._traffic_directors = {
            'dynamic.unit.tests.': {
                'A': td,
            }
        }
        zone = Zone('unit.tests.', [])
        records = provider._populate_traffic_directors(zone, lenient=True)
        self.assertEquals(1, len(records))

    def test_dynamic_records_for_A(self):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Empty
        records = provider._dynamic_records_for_A([], {})
        self.assertEquals([], records)

        # Basic
        values = [{
            'value': '1.2.3.4',
        }, {
            'value': '1.2.3.5',
            'weight': 42,
        }]
        records = provider._dynamic_records_for_A(values, {})
        self.assertEquals(2, len(records))
        record = records[0]
        self.assertEquals('1.2.3.4', record.address)
        self.assertEquals(1, record.weight)
        record = records[1]
        self.assertEquals('1.2.3.5', record.address)
        self.assertEquals(42, record.weight)

        # With extras
        records = provider._dynamic_records_for_A(values, {
            'automation': 'manual',
            'eligible': True,
        })
        self.assertEquals(2, len(records))
        record = records[0]
        self.assertEquals('1.2.3.4', record.address)
        self.assertEquals(1, record.weight)
        self.assertEquals('manual', record._automation)
        self.assertTrue(record.eligible)

    def test_dynamic_records_for_AAAA(self):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Empty
        records = provider._dynamic_records_for_AAAA([], {})
        self.assertEquals([], records)

        # Basic
        values = [{
            'value': '2601:644:500:e210:62f8:1dff:feb8:947a',
        }, {
            'value': '2601:644:500:e210:62f8:1dff:feb8:947b',
            'weight': 42,
        }]
        records = provider._dynamic_records_for_AAAA(values, {})
        self.assertEquals(2, len(records))
        record = records[0]
        self.assertEquals('2601:644:500:e210:62f8:1dff:feb8:947a',
                          record.address)
        self.assertEquals(1, record.weight)
        record = records[1]
        self.assertEquals('2601:644:500:e210:62f8:1dff:feb8:947b',
                          record.address)
        self.assertEquals(42, record.weight)

        # With extras
        records = provider._dynamic_records_for_AAAA(values, {
            'automation': 'manual',
            'eligible': True,
        })
        self.assertEquals(2, len(records))
        record = records[0]
        self.assertEquals('2601:644:500:e210:62f8:1dff:feb8:947a',
                          record.address)
        self.assertEquals(1, record.weight)
        self.assertEquals('manual', record._automation)
        self.assertTrue(record.eligible)

    def test_dynamic_records_for_CNAME(self):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        # Empty
        records = provider._dynamic_records_for_CNAME([], {})
        self.assertEquals([], records)

        # Basic
        values = [{
            'value': 'target-1.unit.tests.',
        }, {
            'value': 'target-2.unit.tests.',
            'weight': 42,
        }]
        records = provider._dynamic_records_for_CNAME(values, {})
        self.assertEquals(2, len(records))
        record = records[0]
        self.assertEquals('target-1.unit.tests.', record.cname)
        self.assertEquals(1, record.weight)
        record = records[1]
        self.assertEquals('target-2.unit.tests.', record.cname)
        self.assertEquals(42, record.weight)

        # With extras
        records = provider._dynamic_records_for_CNAME(values, {
            'automation': 'manual',
            'eligible': True,
        })
        self.assertEquals(2, len(records))
        record = records[0]
        self.assertEquals('target-1.unit.tests.', record.cname)
        self.assertEquals(1, record.weight)
        self.assertEquals('manual', record._automation)
        self.assertTrue(record.eligible)

    def test_dynamic_value_sort_key(self):
        values = [{
            'value': '1.2.3.1',
        }, {
            'value': '1.2.3.27',
        }, {
            'value': '1.2.3.127',
        }, {
            'value': '1.2.3.2',
        }]

        self.assertEquals([{
            'value': '1.2.3.1',
        }, {
            'value': '1.2.3.127',
        }, {
            'value': '1.2.3.2',
        }, {
            'value': '1.2.3.27',
        }], sorted(values, key=_dynamic_value_sort_key))

    @patch('dyn.tm.services.DSFResponsePool.create')
    def test_find_or_create_dynamic_pools(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass')

        td = 42
        label = 'foo'
        values = [{
            'value': '1.2.3.1',
        }, {
            'value': '1.2.3.127',
        }, {
            'value': '1.2.3.2',
        }, {
            'value': '1.2.3.27',
        }]

        # A Pool with no existing pools, will create
        pools = []
        pool = provider._find_or_create_dynamic_pool(td, pools, label, 'A',
                                                     values)
        self.assertIsInstance(pool, DSFResponsePool)
        self.assertEquals(1, len(pool.rs_chains))
        self.assertEquals(1, len(pool.rs_chains[0].record_sets))
        records = pool.rs_chains[0].record_sets[0].records
        self.assertEquals(4, len(records))
        self.assertEquals([v['value'] for v in values],
                          [r.address for r in records])
        self.assertEquals([1 for r in records], [r.weight for r in records])
        mock.assert_called_once_with(td)

        # Ask for the pool we created above and include it in the canidate list
        mock.reset_mock()
        pools = [pool]
        cached = provider._find_or_create_dynamic_pool(td, pools, label, 'A',
                                                       values)
        self.assertEquals(pool, cached)
        mock.assert_not_called()

        # Invalid candidate pool, still finds the valid one that's there too
        mock.reset_mock()
        invalid = DSFResponsePool(label, rs_chains=[])
        pools = [invalid, pool]
        cached = provider._find_or_create_dynamic_pool(td, pools, label, 'A',
                                                       values)
        self.assertEquals(pool, cached)
        mock.assert_not_called()

        # Ask for a pool with a different label, should create a new one
        mock.reset_mock()
        pools = [pool]
        other = provider._find_or_create_dynamic_pool(td, pools, 'other', 'A',
                                                      values)
        self.assertEquals('other', other.label)
        mock.assert_called_once_with(td)

        # Ask for a pool that matches label-wise, but has different values
        values = [{
            'value': '1.2.3.44',
        }]
        mock.reset_mock()
        pools = [pool]
        new = provider._find_or_create_dynamic_pool(td, pools, label, 'A',
                                                    values)
        self.assertEquals(label, new.label)
        self.assertEquals(1, len(new.rs_chains))
        self.assertEquals(1, len(new.rs_chains[0].record_sets))
        records = new.rs_chains[0].record_sets[0].records
        self.assertEquals(1, len(records))
        self.assertEquals([v['value'] for v in values],
                          [r.address for r in records])
        self.assertEquals([1 for r in records], [r.weight for r in records])
        mock.assert_called_once_with(td)

    zone = Zone('unit.tests.', [])
    dynamic_a_record = Record.new(zone, '', {
        'dynamic': {
            'pools': {
                'one': {
                    'values': [{
                        'value': '3.3.3.3',
                    }],
                },
                'two': {
                    # Testing out of order value sorting here
                    'values': [{
                        'value': '5.5.5.5',
                    }, {
                        'value': '4.4.4.4',
                    }],
                },
                'three': {
                    'fallback': 'two',
                    'values': [{
                        'weight': 10,
                        'value': '4.4.4.4',
                    }, {
                        'weight': 12,
                        'value': '5.5.5.5',
                    }],
                },
            },
            'rules': [{
                'geos': ['AF', 'EU', 'AS-JP'],
                'pool': 'three',
            }, {
                'geos': ['NA-US-CA'],
                'pool': 'two',
            }, {
                'pool': 'one',
            }],
        },
        'type': 'A',
        'ttl': 60,
        'values': [
            '1.1.1.1',
            '2.2.2.2',
        ],
    })
    geo_a_record = Record.new(zone, '', {
        'geo': {
            'AF': ['2.2.3.4', '2.2.3.5'],
            'AS-JP': ['3.2.3.4', '3.2.3.5'],
            'NA-US': ['4.2.3.4', '4.2.3.5'],
            'NA-US-CA': ['5.2.3.4', '5.2.3.5']
        },
        'ttl': 300,
        'type': 'A',
        'values': ['1.2.3.4', '1.2.3.5'],
    })
    regular_a_record = Record.new(zone, '', {
        'ttl': 301,
        'type': 'A',
        'value': '1.2.3.4',
    })
    dynamic_cname_record = Record.new(zone, 'www', {
        'dynamic': {
            'pools': {
                'one': {
                    'values': [{
                        'value': 'target-0.unit.tests.',
                    }],
                },
                'two': {
                    # Testing out of order value sorting here
                    'values': [{
                        'value': 'target-1.unit.tests.',
                    }, {
                        'value': 'target-2.unit.tests.',
                    }],
                },
                'three': {
                    'values': [{
                        'weight': 10,
                        'value': 'target-3.unit.tests.',
                    }, {
                        'weight': 12,
                        'value': 'target-4.unit.tests.',
                    }],
                },
            },
            'rules': [{
                'geos': ['AF', 'EU', 'AS-JP'],
                'pool': 'three',
            }, {
                'geos': ['NA-US-CA'],
                'pool': 'two',
            }, {
                'pool': 'one',
            }],
        },
        'type': 'CNAME',
        'ttl': 60,
        'value': 'target.unit.tests.',
    })

    dynamic_fallback_loop = Record.new(zone, '', {
        'dynamic': {
            'pools': {
                'one': {
                    'values': [{
                        'value': '3.3.3.3',
                    }],
                },
                'two': {
                    # Testing out of order value sorting here
                    'fallback': 'three',
                    'values': [{
                        'value': '5.5.5.5',
                    }, {
                        'value': '4.4.4.4',
                    }],
                },
                'three': {
                    'fallback': 'two',
                    'values': [{
                        'weight': 10,
                        'value': '4.4.4.4',
                    }, {
                        'weight': 12,
                        'value': '5.5.5.5',
                    }],
                },
            },
            'rules': [{
                'geos': ['AF', 'EU', 'AS-JP'],
                'pool': 'three',
            }, {
                'geos': ['NA-US-CA'],
                'pool': 'two',
            }, {
                'pool': 'one',
            }],
        },
        'type': 'A',
        'ttl': 60,
        'values': [
            '1.1.1.1',
            '2.2.2.2',
        ],
    }, lenient=True)

    @patch('dyn.tm.services.DSFRuleset.add_response_pool')
    @patch('dyn.tm.services.DSFRuleset.create')
    # just lets us ignore the pool.create calls
    @patch('dyn.tm.services.DSFResponsePool.create')
    def test_mod_dynamic_rulesets_create_CNAME(self, _, ruleset_create_mock,
                                               add_response_pool_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        td_mock = MagicMock()
        td_mock._rulesets = []
        provider._traffic_director_monitor = MagicMock()
        provider._find_or_create_dynamic_pool = MagicMock()

        td_mock.all_response_pools = []

        provider._find_or_create_dynamic_pool.side_effect = [
            _DummyPool('default'),
            _DummyPool('one'),
            _DummyPool('two'),
            _DummyPool('three'),
        ]

        change = Create(self.dynamic_cname_record)
        provider._mod_dynamic_rulesets(td_mock, change)
        add_response_pool_mock.assert_has_calls((
            # default
            call('default'),
            # first dynamic and it's fallback
            call('one'),
            call('default', index=999),
            # 2nd dynamic and it's fallback
            call('three'),
            call('default', index=999),
            # 3nd dynamic and it's fallback
            call('two'),
            call('default', index=999),
        ))
        ruleset_create_mock.assert_has_calls((
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
            call(td_mock, index=0),
        ))

    # have to patch the place it's imported into, not where it lives
    @patch('octodns.provider.dyn.get_response_pool')
    @patch('dyn.tm.services.DSFRuleset.add_response_pool')
    @patch('dyn.tm.services.DSFRuleset.create')
    # just lets us ignore the pool.create calls
    @patch('dyn.tm.services.DSFResponsePool.create')
    def test_mod_dynamic_rulesets_existing(self, _, ruleset_create_mock,
                                           add_response_pool_mock,
                                           get_response_pool_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        ruleset_mock = MagicMock()
        ruleset_mock.response_pools = [_DummyPool('three')]

        td_mock = MagicMock()
        td_mock._rulesets = [
            ruleset_mock,
        ]
        provider._traffic_director_monitor = MagicMock()
        provider._find_or_create_dynamic_pool = MagicMock()
        # Matching ttl
        td_mock.ttl = self.dynamic_a_record.ttl

        unused_pool = _DummyPool('unused')
        td_mock.all_response_pools = \
            ruleset_mock.response_pools + [unused_pool]
        get_response_pool_mock.return_value = unused_pool

        provider._find_or_create_dynamic_pool.side_effect = [
            _DummyPool('default'),
            _DummyPool('one'),
            _DummyPool('two'),
            ruleset_mock.response_pools[0],
        ]

        change = Create(self.dynamic_a_record)
        provider._mod_dynamic_rulesets(td_mock, change)
        add_response_pool_mock.assert_has_calls((
            # default
            call('default'),
            # first dynamic and it's fallback
            call('one'),
            call('default', index=999),
            # 2nd dynamic and it's fallback
            call('three'),
            call('default', index=999),
            # 3nd dynamic, from existing, and it's fallback
            call('two'),
            call('three', index=999),
            call('default', index=999),
        ))
        ruleset_create_mock.assert_has_calls((
            call(td_mock, index=2),
            call(td_mock, index=2),
            call(td_mock, index=2),
            call(td_mock, index=2),
        ))
        # unused poll should have been deleted
        self.assertTrue(unused_pool.deleted)
        # old ruleset ruleset should be deleted, it's pool will have been
        # reused
        ruleset_mock.delete.assert_called_once()

    # have to patch the place it's imported into, not where it lives
    @patch('octodns.provider.dyn.get_response_pool')
    @patch('dyn.tm.services.DSFRuleset.add_response_pool')
    @patch('dyn.tm.services.DSFRuleset.create')
    # just lets us ignore the pool.create calls
    @patch('dyn.tm.services.DSFResponsePool.create')
    def test_mod_dynamic_rulesets_fallback_loop(self, _, ruleset_create_mock,
                                                add_response_pool_mock,
                                                get_response_pool_mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        ruleset_mock = MagicMock()
        ruleset_mock.response_pools = [_DummyPool('three')]

        td_mock = MagicMock()
        td_mock._rulesets = [
            ruleset_mock,
        ]
        provider._traffic_director_monitor = MagicMock()
        provider._find_or_create_dynamic_pool = MagicMock()
        # Matching ttl
        td_mock.ttl = self.dynamic_fallback_loop.ttl

        unused_pool = _DummyPool('unused')
        td_mock.all_response_pools = \
            ruleset_mock.response_pools + [unused_pool]
        get_response_pool_mock.return_value = unused_pool

        provider._find_or_create_dynamic_pool.side_effect = [
            _DummyPool('default'),
            _DummyPool('one'),
            _DummyPool('two'),
            ruleset_mock.response_pools[0],
        ]

        change = Create(self.dynamic_fallback_loop)
        provider._mod_dynamic_rulesets(td_mock, change)
        add_response_pool_mock.assert_has_calls((
            # default
            call('default'),
            # first dynamic and it's fallback
            call('one'),
            call('default', index=999),
            # 2nd dynamic and it's fallback (no loop)
            call('three'),
            call('two', index=999),
            call('default', index=999),
            # 3nd dynamic and it's fallback (no loop)
            call('two'),
            call('three', index=999),
            call('default', index=999),
        ))
        ruleset_create_mock.assert_has_calls((
            call(td_mock, index=2),
            call(td_mock, index=2),
            call(td_mock, index=2),
            call(td_mock, index=2),
        ))
        # unused poll should have been deleted
        self.assertTrue(unused_pool.deleted)
        # old ruleset ruleset should be deleted, it's pool will have been
        # reused
        ruleset_mock.delete.assert_called_once()

    with open('./tests/fixtures/dyn-traffic-director-get.json') as fh:
        traffic_director_response = loads(fh.read())

    @property
    def traffic_directors_response(self):
        return {
            'data': [{
                'active': 'Y',
                'label': 'unit.tests.:A',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '2ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }, {
                'active': 'Y',
                'label': 'some.other.:A',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '3ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }, {
                'active': 'Y',
                'label': 'other format',
                'nodes': [],
                'notifiers': [],
                'pending_change': '',
                'rulesets': [],
                'service_id': '4ERWXQNsb_IKG2YZgYqkPvk0PBM',
                'ttl': '300'
            }]
        }

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_dynamic_create(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # will be tested separately
        provider._mod_dynamic_rulesets = MagicMock()

        mock.side_effect = [
            # create traffic director
            self.traffic_director_response,
            # get traffic directors
            self.traffic_directors_response
        ]
        provider._mod_dynamic_Create(None, Create(self.dynamic_a_record))
        # td now lives in cache
        self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
        # should have seen 1 gen call
        provider._mod_dynamic_rulesets.assert_called_once()

    def test_mod_dynamic_update_dynamic_dynamic(self):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # update of an existing dynamic td

        # pre-populate the cache with our mock td
        provider._traffic_directors = {
            'unit.tests.': {
                'A': 42,
            }
        }
        # mock _mod_dynamic_rulesets
        provider._mod_dynamic_rulesets = MagicMock()

        dyn = self.dynamic_a_record
        change = Update(dyn, dyn)
        provider._mod_dynamic_Update(None, change)
        # still in cache
        self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
        # should have seen 1 gen call
        provider._mod_dynamic_rulesets.assert_called_once_with(42, change)

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_dynamic_update_dynamic_geo(self, _):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # convert a dynamic td to a geo record

        provider._mod_geo_Update = MagicMock()

        change = Update(self.dynamic_a_record, self.geo_a_record)
        provider._mod_dynamic_Update(42, change)
        # should have seen a call to create the new geo record
        provider._mod_geo_Update.assert_called_once_with(42, change)

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_dynamic_update_dynamic_regular(self, _):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # convert a dynamic td to a regular record

        provider._mod_Create = MagicMock()
        provider._mod_dynamic_Delete = MagicMock()

        change = Update(self.dynamic_a_record, self.regular_a_record)
        provider._mod_dynamic_Update(42, change)
        # should have seen a call to create the new regular record
        provider._mod_Create.assert_called_once_with(42, change)
        # should have seen a call to delete the old td record
        provider._mod_dynamic_Delete.assert_called_once_with(42, change)

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_dynamic_update_geo_dynamic(self, _):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # convert a geo record to a dynamic td

        # pre-populate the cache with our mock td
        provider._traffic_directors = {
            'unit.tests.': {
                'A': 42,
            }
        }
        # mock _mod_dynamic_rulesets
        provider._mod_dynamic_rulesets = MagicMock()

        change = Update(self.geo_a_record, self.dynamic_a_record)
        provider._mod_dynamic_Update(None, change)
        # still in cache
        self.assertTrue('A' in provider.traffic_directors['unit.tests.'])
        # should have seen 1 gen call
        provider._mod_dynamic_rulesets.assert_called_once_with(42, change)

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_dynamic_update_regular_dynamic(self, _):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # convert a regular record to a dynamic td

        provider._mod_dynamic_Create = MagicMock()
        provider._mod_Delete = MagicMock()

        change = Update(self.regular_a_record, self.dynamic_a_record)
        provider._mod_dynamic_Update(42, change)
        # should have seen a call to create the new geo record
        provider._mod_dynamic_Create.assert_called_once_with(42, change)
        # should have seen a call to delete the old regular record
        provider._mod_Delete.assert_called_once_with(42, change)

    @patch('dyn.core.SessionEngine.execute')
    def test_mod_dynamic_delete(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        td_mock = MagicMock()
        provider._traffic_directors = {
            'unit.tests.': {
                'A': td_mock,
            }
        }
        provider._mod_dynamic_Delete(None, Delete(self.dynamic_a_record))
        # delete called
        td_mock.delete.assert_called_once()
        # removed from cache
        self.assertFalse('A' in provider.traffic_directors['unit.tests.'])

    @patch('dyn.core.SessionEngine.execute')
    def test_apply_traffic_directors_dynamic(self, mock):
        provider = DynProvider('test', 'cust', 'user', 'pass',
                               traffic_directors_enabled=True)

        # will be tested separately
        provider._mod_dynamic_Create = MagicMock()

        changes = [Create(self.dynamic_a_record)]
        provider._apply_traffic_directors(self.zone, changes, None)
        provider._mod_dynamic_Create.assert_called_once()
