# Copyright (c) 2016 Mirantis, Inc.
# All Rights Reserved.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import itertools
import mock
import os_vif.objects.network as osv_network
import os_vif.objects.subnet as osv_subnet
from oslo_utils import uuidutils

from kuryr_kubernetes import constants as k_const
from kuryr_kubernetes.controller.drivers import base as drv_base
from kuryr_kubernetes.controller.handlers import lbaas as h_lbaas
from kuryr_kubernetes import exceptions as k_exc
from kuryr_kubernetes.objects import lbaas as obj_lbaas
from kuryr_kubernetes.tests import base as test_base

_SUPPORTED_LISTENER_PROT = ('HTTP', 'HTTPS', 'TCP')


class TestLBaaSSpecHandler(test_base.TestCase):

    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.ServiceSecurityGroupsDriver.get_instance')
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.ServiceSubnetsDriver.get_instance')
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.ServiceProjectDriver.get_instance')
    def test_init(self, m_get_drv_project, m_get_drv_subnets, m_get_drv_sg):
        m_get_drv_project.return_value = mock.sentinel.drv_project
        m_get_drv_subnets.return_value = mock.sentinel.drv_subnets
        m_get_drv_sg.return_value = mock.sentinel.drv_sg
        handler = h_lbaas.LBaaSSpecHandler()

        self.assertEqual(mock.sentinel.drv_project, handler._drv_project)
        self.assertEqual(mock.sentinel.drv_subnets, handler._drv_subnets)
        self.assertEqual(mock.sentinel.drv_sg, handler._drv_sg)

    def test_on_present(self):
        svc_event = mock.sentinel.svc_event
        old_spec = mock.sentinel.old_spec
        new_spec = mock.sentinel.new_spec

        project_id = mock.sentinel.project_id
        m_drv_project = mock.Mock()
        m_drv_project.get_project.return_value = project_id

        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        m_handler._get_lbaas_spec.return_value = old_spec
        m_handler._has_lbaas_spec_changes.return_value = True
        m_handler._generate_lbaas_spec.return_value = new_spec
        m_handler._should_ignore.return_value = False
        m_handler._drv_project = m_drv_project

        h_lbaas.LBaaSSpecHandler.on_present(m_handler, svc_event)

        m_handler._get_lbaas_spec.assert_called_once_with(svc_event)
        m_handler._has_lbaas_spec_changes.assert_called_once_with(svc_event,
                                                                  old_spec)
        m_handler._generate_lbaas_spec.assert_called_once_with(svc_event)
        m_handler._set_lbaas_spec.assert_called_once_with(svc_event, new_spec)

    def test_on_present_no_changes(self):
        svc_event = mock.sentinel.svc_event
        old_spec = mock.sentinel.old_spec

        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        m_handler._get_lbaas_spec.return_value = old_spec
        m_handler._has_lbaas_spec_changes.return_value = False
        m_handler._should_ignore.return_value = False

        h_lbaas.LBaaSSpecHandler.on_present(m_handler, svc_event)

        m_handler._get_lbaas_spec.assert_called_once_with(svc_event)
        m_handler._has_lbaas_spec_changes.assert_called_once_with(svc_event,
                                                                  old_spec)
        m_handler._generate_lbaas_spec.assert_not_called()
        m_handler._set_lbaas_spec.assert_not_called()

    def test_on_present_no_selector(self):
        svc_event = mock.sentinel.svc_event
        old_spec = mock.sentinel.old_spec

        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        m_handler._get_lbaas_spec.return_value = old_spec
        m_handler._should_ignore.return_value = True

        h_lbaas.LBaaSSpecHandler.on_present(m_handler, svc_event)

        m_handler._get_lbaas_spec.assert_called_once_with(svc_event)
        m_handler._has_lbaas_spec_changes.assert_not_called()
        m_handler._generate_lbaas_spec.assert_not_called()
        m_handler._set_lbaas_spec.assert_not_called()

    def test_get_service_ip(self):
        svc_body = {'spec': {'type': 'ClusterIP',
                             'clusterIP': mock.sentinel.cluster_ip}}
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)

        ret = h_lbaas.LBaaSSpecHandler._get_service_ip(m_handler, svc_body)
        self.assertEqual(mock.sentinel.cluster_ip, ret)

        svc_body = {'spec': {'type': 'LoadBalancer',
                             'clusterIP': mock.sentinel.cluster_ip}}
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)

        ret = h_lbaas.LBaaSSpecHandler._get_service_ip(m_handler, svc_body)
        self.assertEqual(mock.sentinel.cluster_ip, ret)

    def test_is_supported_type_clusterip(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        svc_body = {'spec': {'type': 'ClusterIP',
                             'clusterIP': mock.sentinel.cluster_ip}}

        ret = h_lbaas.LBaaSSpecHandler._is_supported_type(m_handler, svc_body)
        self.assertEqual(ret, True)

    def test_is_supported_type_loadbalancer(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        svc_body = {'spec': {'type': 'LoadBalancer',
                             'clusterIP': mock.sentinel.cluster_ip}}

        ret = h_lbaas.LBaaSSpecHandler._is_supported_type(m_handler, svc_body)
        self.assertEqual(ret, True)

    def _make_test_net_obj(self, cidr_list):
        subnets = [osv_subnet.Subnet(cidr=cidr) for cidr in cidr_list]
        subnets_list = osv_subnet.SubnetList(objects=subnets)
        return osv_network.Network(subnets=subnets_list)

    def test_generate_lbaas_spec(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)

        service = mock.sentinel.service
        project_id = mock.sentinel.project_id
        ip = mock.sentinel.ip
        subnet_id = mock.sentinel.subnet_id
        ports = mock.sentinel.ports
        sg_ids = mock.sentinel.sg_ids

        m_drv_project = mock.Mock()
        m_drv_project.get_project.return_value = project_id
        m_drv_sg = mock.Mock()
        m_drv_sg.get_security_groups.return_value = sg_ids
        m_handler._drv_project = m_drv_project
        m_handler._drv_sg = m_drv_sg
        m_handler._get_service_ip.return_value = ip
        m_handler._get_subnet_id.return_value = subnet_id
        m_handler._generate_lbaas_port_specs.return_value = ports

        spec_ctor_path = 'kuryr_kubernetes.objects.lbaas.LBaaSServiceSpec'
        with mock.patch(spec_ctor_path) as m_spec_ctor:
            m_spec_ctor.return_value = mock.sentinel.ret_obj
            service = {'spec': {'type': 'ClusterIP'}}

            ret_obj = h_lbaas.LBaaSSpecHandler._generate_lbaas_spec(
                m_handler, service)
            self.assertEqual(mock.sentinel.ret_obj, ret_obj)
            m_spec_ctor.assert_called_once_with(
                ip=ip,
                project_id=project_id,
                subnet_id=subnet_id,
                ports=ports,
                security_groups_ids=sg_ids,
                type='ClusterIP',
                lb_ip=None)

        m_drv_project.get_project.assert_called_once_with(service)
        m_handler._get_service_ip.assert_called_once_with(service)
        m_handler._get_subnet_id.assert_called_once_with(
            service, project_id, ip)
        m_handler._generate_lbaas_port_specs.assert_called_once_with(service)
        m_drv_sg.get_security_groups.assert_called_once_with(
            service, project_id)

    def test_has_lbaas_spec_changes(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        service = mock.sentinel.service
        lbaas_spec = mock.sentinel.lbaas_spec

        for has_ip_changes in (True, False):
            for has_port_changes in (True, False):
                m_handler._has_ip_changes.return_value = has_ip_changes
                m_handler._has_port_changes.return_value = has_port_changes
                ret = h_lbaas.LBaaSSpecHandler._has_lbaas_spec_changes(
                    m_handler, service, lbaas_spec)
                self.assertEqual(has_ip_changes or has_port_changes, ret)

    def test_get_service_ports(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        service = {'spec': {'ports': [
            {'port': 1},
            {'port': 2, 'name': 'X', 'protocol': 'UDP'}
        ]}}
        expected_ret = [
            {'port': 1, 'name': None, 'protocol': 'TCP'},
            {'port': 2, 'name': 'X', 'protocol': 'UDP'}]

        ret = h_lbaas.LBaaSSpecHandler._get_service_ports(m_handler, service)
        self.assertEqual(expected_ret, ret)

    def test_has_port_changes(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        m_service = mock.MagicMock()
        m_handler._get_service_ports.return_value = [
            {'port': 1, 'name': 'X', 'protocol': 'TCP'},
        ]

        m_lbaas_spec = mock.MagicMock()
        m_lbaas_spec.ports = [
            obj_lbaas.LBaaSPortSpec(name='X', protocol='TCP', port=1),
            obj_lbaas.LBaaSPortSpec(name='Y', protocol='TCP', port=2),
        ]

        ret = h_lbaas.LBaaSSpecHandler._has_port_changes(
            m_handler, m_service, m_lbaas_spec)

        self.assertTrue(ret)

    def test_has_port_changes__no_changes(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        m_service = mock.MagicMock()
        m_handler._get_service_ports.return_value = [
            {'port': 1, 'name': 'X', 'protocol': 'TCP'},
            {'port': 2, 'name': 'Y', 'protocol': 'TCP'}
        ]

        m_lbaas_spec = mock.MagicMock()
        m_lbaas_spec.ports = [
            obj_lbaas.LBaaSPortSpec(name='X', protocol='TCP', port=1),
            obj_lbaas.LBaaSPortSpec(name='Y', protocol='TCP', port=2),
        ]

        ret = h_lbaas.LBaaSSpecHandler._has_port_changes(
            m_handler, m_service, m_lbaas_spec)

        self.assertFalse(ret)

    def test_has_ip_changes(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        m_service = mock.MagicMock()
        m_handler._get_service_ip.return_value = '1.1.1.1'
        m_lbaas_spec = mock.MagicMock()
        m_lbaas_spec.ip.__str__.return_value = '2.2.2.2'

        ret = h_lbaas.LBaaSSpecHandler._has_ip_changes(
            m_handler, m_service, m_lbaas_spec)
        self.assertTrue(ret)

    def test_has_ip_changes__no_changes(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        m_service = mock.MagicMock()
        m_handler._get_service_ip.return_value = '1.1.1.1'
        m_lbaas_spec = mock.MagicMock()
        m_lbaas_spec.ip.__str__.return_value = '1.1.1.1'

        ret = h_lbaas.LBaaSSpecHandler._has_ip_changes(
            m_handler, m_service, m_lbaas_spec)
        self.assertFalse(ret)

    def test_has_ip_changes__no_spec(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        m_service = mock.MagicMock()
        m_handler._get_service_ip.return_value = '1.1.1.1'
        m_lbaas_spec = None

        ret = h_lbaas.LBaaSSpecHandler._has_ip_changes(
            m_handler, m_service, m_lbaas_spec)
        self.assertTrue(ret)

    def test_has_ip_changes__no_nothing(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        m_service = mock.MagicMock()
        m_handler._get_service_ip.return_value = None
        m_lbaas_spec = None

        ret = h_lbaas.LBaaSSpecHandler._has_ip_changes(
            m_handler, m_service, m_lbaas_spec)
        self.assertFalse(ret)

    def test_generate_lbaas_port_specs(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        m_handler._get_service_ports.return_value = [
            {'port': 1, 'name': 'X', 'protocol': 'TCP'},
            {'port': 2, 'name': 'Y', 'protocol': 'TCP'}
        ]
        expected_ports = [
            obj_lbaas.LBaaSPortSpec(name='X', protocol='TCP', port=1),
            obj_lbaas.LBaaSPortSpec(name='Y', protocol='TCP', port=2),
        ]

        ret = h_lbaas.LBaaSSpecHandler._generate_lbaas_port_specs(
            m_handler, mock.sentinel.service)
        self.assertEqual(expected_ports, ret)
        m_handler._get_service_ports.assert_called_once_with(
            mock.sentinel.service)

    def test_generate_lbaas_port_specs_udp(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        m_handler._get_service_ports.return_value = [
            {'port': 1, 'name': 'X', 'protocol': 'TCP'},
            {'port': 2, 'name': 'Y', 'protocol': 'UDP'}
        ]
        expected_ports = [
            obj_lbaas.LBaaSPortSpec(name='X', protocol='TCP', port=1),
            obj_lbaas.LBaaSPortSpec(name='Y', protocol='UDP', port=2),
        ]

        ret = h_lbaas.LBaaSSpecHandler._generate_lbaas_port_specs(
            m_handler, mock.sentinel.service)
        self.assertEqual(expected_ports, ret)
        m_handler._get_service_ports.assert_called_once_with(
            mock.sentinel.service)

    def test_get_endpoints_link(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        service = {'metadata': {
            'selfLink': "/api/v1/namespaces/default/services/test"}}
        ret = h_lbaas.LBaaSSpecHandler._get_endpoints_link(m_handler, service)
        expected_link = "/api/v1/namespaces/default/endpoints/test"
        self.assertEqual(expected_link, ret)

    def test_get_endpoints_link__integrity_error(self):
        m_handler = mock.Mock(spec=h_lbaas.LBaaSSpecHandler)
        service = {'metadata': {
            'selfLink': "/api/v1/namespaces/default/not-services/test"}}
        self.assertRaises(k_exc.IntegrityError,
                          h_lbaas.LBaaSSpecHandler._get_endpoints_link,
                          m_handler, service)

    def test_set_lbaas_spec(self):
        self.skipTest("skipping until generalised annotation handling is "
                      "implemented")

    def test_get_lbaas_spec(self):
        self.skipTest("skipping until generalised annotation handling is "
                      "implemented")


class FakeLBaaSDriver(drv_base.LBaaSDriver):

    def ensure_loadbalancer(self, endpoints, project_id, subnet_id, ip,
                            security_groups_ids, service_type):
        name = str(ip)
        return obj_lbaas.LBaaSLoadBalancer(name=name,
                                           project_id=project_id,
                                           subnet_id=subnet_id,
                                           ip=ip,
                                           id=uuidutils.generate_uuid())

    def ensure_listener(self, endpoints, loadbalancer, protocol, port):
        if protocol not in _SUPPORTED_LISTENER_PROT:
            return None

        name = "%s:%s:%s" % (loadbalancer.name, protocol, port)
        return obj_lbaas.LBaaSListener(name=name,
                                       project_id=loadbalancer.project_id,
                                       loadbalancer_id=loadbalancer.id,
                                       protocol=protocol,
                                       port=port,
                                       id=uuidutils.generate_uuid())

    def ensure_pool(self, endpoints, loadbalancer, listener):
        return obj_lbaas.LBaaSPool(name=listener.name,
                                   project_id=loadbalancer.project_id,
                                   loadbalancer_id=loadbalancer.id,
                                   listener_id=listener.id,
                                   protocol=listener.protocol,
                                   id=uuidutils.generate_uuid())

    def ensure_member(self, endpoints, loadbalancer, pool, subnet_id, ip, port,
                      target_ref):
        name = "%s:%s:%s" % (loadbalancer.name, ip, port)
        return obj_lbaas.LBaaSMember(name=name,
                                     project_id=pool.project_id,
                                     pool_id=pool.id,
                                     subnet_id=subnet_id,
                                     ip=ip,
                                     port=port,
                                     id=uuidutils.generate_uuid())

    def release_loadbalancer(self, endpoints, loadbalancer):
        pass

    def release_listener(self, endpoints, loadbalancer, listener):
        pass

    def release_pool(self, endpoints, loadbalancer, pool):
        pass

    def release_member(self, endpoints, loadbalancer, member):
        pass


class TestLoadBalancerHandler(test_base.TestCase):
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.ServicePubIpDriver.get_instance')
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.PodSubnetsDriver.get_instance')
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.PodProjectDriver.get_instance')
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.LBaaSDriver.get_instance')
    def test_init(self, m_get_drv_lbaas, m_get_drv_project,
                  m_get_drv_subnets, m_get_drv_service_pub_ip):
        m_get_drv_lbaas.return_value = mock.sentinel.drv_lbaas
        m_get_drv_project.return_value = mock.sentinel.drv_project
        m_get_drv_subnets.return_value = mock.sentinel.drv_subnets
        m_get_drv_service_pub_ip.return_value = mock.sentinel.drv_lb_ip
        handler = h_lbaas.LoadBalancerHandler()

        self.assertEqual(mock.sentinel.drv_lbaas, handler._drv_lbaas)
        self.assertEqual(mock.sentinel.drv_project, handler._drv_pod_project)
        self.assertEqual(mock.sentinel.drv_subnets, handler._drv_pod_subnets)
        self.assertEqual(mock.sentinel.drv_lb_ip, handler._drv_service_pub_ip)

    def test_on_present(self):
        lbaas_spec = mock.sentinel.lbaas_spec
        lbaas_state = mock.sentinel.lbaas_state
        lbaas_state.service_pub_ip_info = None
        endpoints = mock.sentinel.endpoints

        m_handler = mock.Mock(spec=h_lbaas.LoadBalancerHandler)
        m_handler._get_lbaas_spec.return_value = lbaas_spec
        m_handler._should_ignore.return_value = False
        m_handler._get_lbaas_state.return_value = lbaas_state
        m_handler._sync_lbaas_members.return_value = True

        h_lbaas.LoadBalancerHandler.on_present(m_handler, endpoints)

        m_handler._get_lbaas_spec.assert_called_once_with(endpoints)
        m_handler._should_ignore.assert_called_once_with(endpoints, lbaas_spec)
        m_handler._get_lbaas_state.assert_called_once_with(endpoints)
        m_handler._sync_lbaas_members.assert_called_once_with(
            endpoints, lbaas_state, lbaas_spec)
        m_handler._set_lbaas_state.assert_called_once_with(
            endpoints, lbaas_state)
        m_handler._update_lb_status.assert_not_called()

    def _fake_sync_lbaas_members(self, endpoints, lbaas_state, lbaas_spec):
        floating_ip = {'floating_ip_address': '1.2.3.5',
                       'id': 'ec29d641-fec4-4f67-928a-124a76b3a888'}

        service_pub_ip_info = obj_lbaas.LBaaSPubIp(
            ip_id=floating_ip['id'],
            ip_addr=floating_ip['floating_ip_address'], alloc_method='kk')

        lbaas_state.service_pub_ip_info = service_pub_ip_info
        return True

    def test_on_present_loadbalancer_service(self):
        lbaas_spec = mock.sentinel.lbaas_spec
        lbaas_state = mock.sentinel.lbaas_state
        lbaas_state.service_pub_ip_info = None
        endpoints = mock.sentinel.endpoints

        m_handler = mock.Mock(spec=h_lbaas.LoadBalancerHandler)
        m_handler._get_lbaas_spec.return_value = lbaas_spec
        m_handler._should_ignore.return_value = False
        m_handler._get_lbaas_state.return_value = lbaas_state
        m_handler._sync_lbaas_members = self._fake_sync_lbaas_members

        h_lbaas.LoadBalancerHandler.on_present(m_handler, endpoints)

        m_handler._get_lbaas_spec.assert_called_once_with(endpoints)
        m_handler._should_ignore.assert_called_once_with(endpoints, lbaas_spec)
        m_handler._get_lbaas_state.assert_called_once_with(endpoints)
        m_handler._set_lbaas_state.assert_called_once_with(
            endpoints, lbaas_state)
        m_handler._update_lb_status.assert_called()

    def test_on_present_rollback(self):
        lbaas_spec = mock.sentinel.lbaas_spec
        lbaas_state = mock.sentinel.lbaas_state
        lbaas_state.service_pub_ip_info = None
        endpoints = mock.sentinel.endpoints

        m_handler = mock.Mock(spec=h_lbaas.LoadBalancerHandler)
        m_handler._get_lbaas_spec.return_value = lbaas_spec
        m_handler._should_ignore.return_value = False
        m_handler._get_lbaas_state.return_value = lbaas_state
        m_handler._sync_lbaas_members.return_value = True
        m_handler._set_lbaas_state.side_effect = (
            k_exc.K8sResourceNotFound('ep'))

        h_lbaas.LoadBalancerHandler.on_present(m_handler, endpoints)

        m_handler._get_lbaas_spec.assert_called_once_with(endpoints)
        m_handler._should_ignore.assert_called_once_with(endpoints, lbaas_spec)
        m_handler._get_lbaas_state.assert_called_once_with(endpoints)
        m_handler._sync_lbaas_members.assert_called_once_with(
            endpoints, lbaas_state, lbaas_spec)
        m_handler._set_lbaas_state.assert_called_once_with(
            endpoints, lbaas_state)
        m_handler.on_deleted.assert_called_once_with(
            endpoints, lbaas_state)

    @mock.patch('kuryr_kubernetes.objects.lbaas'
                '.LBaaSServiceSpec')
    def test_on_deleted(self, m_svc_spec_ctor):
        endpoints = mock.sentinel.endpoints
        empty_spec = mock.sentinel.empty_spec
        lbaas_state = mock.sentinel.lbaas_state
        m_svc_spec_ctor.return_value = empty_spec

        m_handler = mock.Mock(spec=h_lbaas.LoadBalancerHandler)
        m_handler._get_lbaas_state.return_value = lbaas_state
        m_handler._drv_lbaas = mock.Mock()
        m_handler._drv_lbaas.cascading_capable = False

        h_lbaas.LoadBalancerHandler.on_deleted(m_handler, endpoints)

        m_handler._get_lbaas_state.assert_called_once_with(endpoints)
        m_handler._sync_lbaas_members.assert_called_once_with(
            endpoints, lbaas_state, empty_spec)

    @mock.patch('kuryr_kubernetes.objects.lbaas'
                '.LBaaSServiceSpec')
    def test_on_cascade_deleted_lb_service(self, m_svc_spec_ctor):
        endpoints = mock.sentinel.endpoints
        empty_spec = mock.sentinel.empty_spec
        lbaas_state = mock.Mock()
        lbaas_state.loadbalancer = mock.sentinel.loadbalancer
        lbaas_state.service_pub_ip_info = mock.sentinel.pub_ip
        m_svc_spec_ctor.return_value = empty_spec

        m_handler = mock.Mock(spec=h_lbaas.LoadBalancerHandler)
        m_handler._get_lbaas_state.return_value = lbaas_state
        m_handler._drv_lbaas = mock.Mock()
        m_handler._drv_service_pub_ip = mock.Mock()
        m_handler._drv_lbaas.cascading_capable = True

        h_lbaas.LoadBalancerHandler.on_deleted(m_handler, endpoints)

        m_handler._drv_lbaas.release_loadbalancer.assert_called_once_with(
            endpoints=endpoints, loadbalancer=lbaas_state.loadbalancer)
        m_handler._drv_service_pub_ip.release_pub_ip.assert_called_once_with(
            lbaas_state.service_pub_ip_info)

    def test_should_ignore(self):
        endpoints = mock.sentinel.endpoints
        lbaas_spec = mock.sentinel.lbaas_spec

        # REVISIT(ivc): ddt?
        m_handler = mock.Mock(spec=h_lbaas.LoadBalancerHandler)
        m_handler._has_pods.return_value = True
        m_handler._is_lbaas_spec_in_sync.return_value = True

        ret = h_lbaas.LoadBalancerHandler._should_ignore(
            m_handler, endpoints, lbaas_spec)
        self.assertEqual(False, ret)

        m_handler._has_pods.assert_called_once_with(endpoints)
        m_handler._is_lbaas_spec_in_sync.assert_called_once_with(
            endpoints, lbaas_spec)

    def test_is_lbaas_spec_in_sync(self):
        names = ['a', 'b', 'c']
        endpoints = {'subsets': [{'ports': [{'name': n} for n in names]}]}
        lbaas_spec = obj_lbaas.LBaaSServiceSpec(ports=[
            obj_lbaas.LBaaSPortSpec(name=n) for n in reversed(names)])

        m_handler = mock.Mock(spec=h_lbaas.LoadBalancerHandler)
        ret = h_lbaas.LoadBalancerHandler._is_lbaas_spec_in_sync(
            m_handler, endpoints, lbaas_spec)

        self.assertEqual(True, ret)

    def test_has_pods(self):
        # REVISIT(ivc): ddt?
        endpoints = {'subsets': [
            {},
            {'addresses': []},
            {'addresses': [{'targetRef': {}}]},
            {'addresses': [{'targetRef': {'kind': k_const.K8S_OBJ_POD}}]}
        ]}

        m_handler = mock.Mock(spec=h_lbaas.LoadBalancerHandler)

        ret = h_lbaas.LoadBalancerHandler._has_pods(m_handler, endpoints)

        self.assertEqual(True, ret)

    def test_get_pod_subnet(self):
        subnet_id = mock.sentinel.subnet_id
        project_id = mock.sentinel.project_id
        target_ref = {'kind': k_const.K8S_OBJ_POD,
                      'name': 'pod-name',
                      'namespace': 'default'}
        ip = '1.2.3.4'
        m_handler = mock.Mock(spec=h_lbaas.LoadBalancerHandler)
        m_drv_pod_project = mock.Mock()
        m_drv_pod_project.get_project.return_value = project_id
        m_handler._drv_pod_project = m_drv_pod_project
        m_drv_pod_subnets = mock.Mock()
        m_drv_pod_subnets.get_subnets.return_value = {
            subnet_id: osv_network.Network(subnets=osv_subnet.SubnetList(
                objects=[osv_subnet.Subnet(cidr='1.2.3.0/24')]))}
        m_handler._drv_pod_subnets = m_drv_pod_subnets

        observed_subnet_id = h_lbaas.LoadBalancerHandler._get_pod_subnet(
            m_handler, target_ref, ip)

        self.assertEqual(subnet_id, observed_subnet_id)

    def _generate_lbaas_state(self, vip, targets, project_id, subnet_id):
        endpoints = mock.sentinel.endpoints
        drv = FakeLBaaSDriver()
        lb = drv.ensure_loadbalancer(
            endpoints, project_id, subnet_id, vip, None, 'ClusterIP')
        listeners = {}
        pools = {}
        members = {}
        for ip, (listen_port, target_port) in targets.items():
            lsnr = listeners.setdefault(listen_port, drv.ensure_listener(
                endpoints, lb, 'TCP', listen_port))
            pool = pools.setdefault(listen_port, drv.ensure_pool(
                endpoints, lb, lsnr))
            members.setdefault((ip, listen_port, target_port),
                               drv.ensure_member(endpoints, lb, pool,
                                                 subnet_id, ip,
                                                 target_port, None))
        return obj_lbaas.LBaaSState(
            loadbalancer=lb,
            listeners=list(listeners.values()),
            pools=list(pools.values()),
            members=list(members.values()))

    def _generate_lbaas_spec(self, vip, targets, project_id,
                             subnet_id, prot='TCP'):
        return obj_lbaas.LBaaSServiceSpec(
            ip=vip,
            project_id=project_id,
            subnet_id=subnet_id,
            ports=[obj_lbaas.LBaaSPortSpec(name=str(port),
                                           protocol=prot,
                                           port=port)
                   for port in set(t[0] for t in targets.values())])

    def _generate_endpoints(self, targets):
        def _target_to_port(item):
            _, (listen_port, target_port) = item
            return {'port': target_port, 'name': str(listen_port)}
        port_with_addrs = [
            (p, [e[0] for e in grp])
            for p, grp in itertools.groupby(
                sorted(targets.items()), _target_to_port)]
        return {
            'subsets': [
                {
                    'addresses': [
                        {
                            'ip': ip,
                            'targetRef': {
                                'kind': k_const.K8S_OBJ_POD,
                                'name': ip,
                                'namespace': 'default'
                            }
                        }
                        for ip in addrs
                    ],
                    'ports': [port]
                }
                for port, addrs in port_with_addrs
            ]
        }

    def _sync_lbaas_members_impl(self, m_get_drv_lbaas, m_get_drv_project,
                                 m_get_drv_subnets, subnet_id, project_id,
                                 endpoints, state, spec):
        m_drv_lbaas = mock.Mock(wraps=FakeLBaaSDriver())
        m_drv_project = mock.Mock()
        m_drv_project.get_project.return_value = project_id
        m_drv_subnets = mock.Mock()
        m_drv_subnets.get_subnets.return_value = {
            subnet_id: mock.sentinel.subnet}
        m_get_drv_lbaas.return_value = m_drv_lbaas
        m_get_drv_project.return_value = m_drv_project
        m_get_drv_subnets.return_value = m_drv_subnets

        handler = h_lbaas.LoadBalancerHandler()

        with mock.patch.object(handler, '_get_pod_subnet') as m_get_pod_subnet:
            m_get_pod_subnet.return_value = subnet_id
            handler._sync_lbaas_members(endpoints, state, spec)

        lsnrs = {lsnr.id: lsnr for lsnr in state.listeners}
        pools = {pool.id: pool for pool in state.pools}
        observed_targets = sorted(
            (str(member.ip), (
                lsnrs[pools[member.pool_id].listener_id].port,
                member.port))
            for member in state.members)
        return observed_targets

    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.PodSubnetsDriver.get_instance')
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.PodProjectDriver.get_instance')
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.LBaaSDriver.get_instance')
    def test_sync_lbaas_members(self, m_get_drv_lbaas, m_get_drv_project,
                                m_get_drv_subnets):
        # REVISIT(ivc): test methods separately and verify ensure/release
        project_id = uuidutils.generate_uuid()
        subnet_id = uuidutils.generate_uuid()
        current_ip = '1.1.1.1'
        current_targets = {
            '1.1.1.101': (1001, 10001),
            '1.1.1.111': (1001, 10001),
            '1.1.1.201': (2001, 20001)}
        expected_ip = '2.2.2.2'
        expected_targets = {
            '2.2.2.101': (1201, 12001),
            '2.2.2.111': (1201, 12001),
            '2.2.2.201': (2201, 22001)}
        endpoints = self._generate_endpoints(expected_targets)
        state = self._generate_lbaas_state(
            current_ip, current_targets, project_id, subnet_id)
        spec = self._generate_lbaas_spec(expected_ip, expected_targets,
                                         project_id, subnet_id)

        observed_targets = self._sync_lbaas_members_impl(
            m_get_drv_lbaas, m_get_drv_project, m_get_drv_subnets,
            subnet_id, project_id, endpoints, state, spec)

        self.assertEqual(sorted(expected_targets.items()), observed_targets)
        self.assertEqual(expected_ip, str(state.loadbalancer.ip))

    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.PodSubnetsDriver.get_instance')
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.PodProjectDriver.get_instance')
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.LBaaSDriver.get_instance')
    def test_sync_lbaas_members_udp(self, m_get_drv_lbaas,
                                    m_get_drv_project, m_get_drv_subnets):
        # REVISIT(ivc): test methods separately and verify ensure/release
        project_id = uuidutils.generate_uuid()
        subnet_id = uuidutils.generate_uuid()
        current_ip = '1.1.1.1'
        current_targets = {
            '1.1.1.101': (1001, 10001),
            '1.1.1.111': (1001, 10001),
            '1.1.1.201': (2001, 20001)}
        expected_ip = '2.2.2.2'
        expected_targets = {
            '2.2.2.101': (1201, 12001),
            '2.2.2.111': (1201, 12001),
            '2.2.2.201': (2201, 22001)}
        endpoints = self._generate_endpoints(expected_targets)
        state = self._generate_lbaas_state(
            current_ip, current_targets, project_id, subnet_id)
        spec = self._generate_lbaas_spec(expected_ip, expected_targets,
                                         project_id, subnet_id, 'UDP')

        observed_targets = self._sync_lbaas_members_impl(
            m_get_drv_lbaas, m_get_drv_project, m_get_drv_subnets,
            subnet_id, project_id, endpoints, state, spec)

        self.assertEqual([], observed_targets)
        self.assertEqual(expected_ip, str(state.loadbalancer.ip))

    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.PodSubnetsDriver.get_instance')
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.PodProjectDriver.get_instance')
    @mock.patch('kuryr_kubernetes.controller.drivers.base'
                '.LBaaSDriver.get_instance')
    def test_sync_lbaas_members_svc_listener_port_edit(
            self, m_get_drv_lbaas, m_get_drv_project, m_get_drv_subnets):
        # REVISIT(ivc): test methods separately and verify ensure/release
        project_id = uuidutils.generate_uuid()
        subnet_id = uuidutils.generate_uuid()
        current_ip = '1.1.1.1'
        current_targets = {
            '1.1.1.101': (1001, 10001)}
        expected_ip = '1.1.1.1'
        expected_targets = {
            '1.1.1.101': (1201, 10001)}
        endpoints = self._generate_endpoints(expected_targets)
        state = self._generate_lbaas_state(
            current_ip, current_targets, project_id, subnet_id)
        spec = self._generate_lbaas_spec(expected_ip, expected_targets,
                                         project_id, subnet_id)

        m_drv_lbaas = mock.Mock(wraps=FakeLBaaSDriver())
        m_drv_project = mock.Mock()
        m_drv_project.get_project.return_value = project_id
        m_drv_subnets = mock.Mock()
        m_drv_subnets.get_subnets.return_value = {
            subnet_id: mock.sentinel.subnet}
        m_get_drv_lbaas.return_value = m_drv_lbaas
        m_get_drv_project.return_value = m_drv_project
        m_get_drv_subnets.return_value = m_drv_subnets

        handler = h_lbaas.LoadBalancerHandler()

        with mock.patch.object(handler, '_get_pod_subnet') as m_get_pod_subnet:
            m_get_pod_subnet.return_value = subnet_id
            handler._sync_lbaas_members(endpoints, state, spec)

        self.assertEqual(expected_ip, str(state.loadbalancer.ip))
        m_drv_lbaas.release_pool.assert_called_once()

    def test_get_lbaas_spec(self):
        self.skipTest("skipping until generalised annotation handling is "
                      "implemented")

    def test_get_lbaas_state(self):
        self.skipTest("skipping until generalised annotation handling is "
                      "implemented")

    def test_set_lbaas_state(self):
        self.skipTest("skipping until generalised annotation handling is "
                      "implemented")
