# Copyright (c) 2015 OpenStack Foundation.
# 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 socket
import ssl
from unittest import mock

import eventlet

from neutron.tests import base

from networking_l2gw.services.l2gateway.agent import l2gateway_config as conf
from networking_l2gw.services.l2gateway.agent.ovsdb import ovsdb_monitor
from networking_l2gw.services.l2gateway.common import config
from networking_l2gw.services.l2gateway.common import constants as n_const
from networking_l2gw.services.l2gateway.common import ovsdb_schema
from networking_l2gw.services.l2gateway import exceptions
from networking_l2gw.tests.unit.services.l2gateway.agent.ovsdb import (
    test_base_connection as base_test)

from oslo_config import cfg
from oslo_log import log as logging
from oslo_serialization import jsonutils

LOG = logging.getLogger(__name__)


class TestOVSDBMonitor(base.BaseTestCase):
    def setUp(self):
        super(TestOVSDBMonitor, self).setUp()

        self.conf = mock.patch.object(conf, 'L2GatewayConfig').start()
        config.register_ovsdb_opts_helper(cfg.CONF)
        self.callback = mock.Mock()
        cfg.CONF.set_override('max_connection_retries', 0, 'ovsdb')

        self.sock = mock.patch('socket.socket').start()
        self.ssl_sock = mock.patch.object(ssl, 'wrap_socket').start()
        self.greenthread = mock.patch.object(eventlet.greenthread,
                                             'spawn_n').start()
        self.l2gw_ovsdb = ovsdb_monitor.OVSDBMonitor(mock.Mock(),
                                                     self.conf,
                                                     self.callback)
        self.op_id = 'abcd'
        props = {'select': {'initial': True,
                            'insert': True,
                            'delete': True,
                            'modify': True}}
        self.monitor_message = {'id': self.op_id,
                                'method': 'monitor',
                                'params': [n_const.OVSDB_SCHEMA_NAME,
                                           None,
                                           {'Logical_Switch': [props],
                                            'Physical_Switch': [props],
                                            'Physical_Port': [props],
                                            'Ucast_Macs_Local': [props],
                                            'Ucast_Macs_Remote': [props],
                                            'Physical_Locator': [props],
                                            'Mcast_Macs_Local': [props],
                                            'Physical_Locator_Set': [props]}]}

        fake_message = {'Logical_Switch': props,
                        'Physical_Switch': props,
                        'Physical_Port': props,
                        'Ucast_Macs_Local': props,
                        'Ucast_Macs_Remote': props,
                        'Physical_Locator': props,
                        'Mcast_Macs_Local': props,
                        'Physical_Locator_Set': props}

        self.msg = self.monitor_message
        self.msg1 = {'result': fake_message}
        self.msg2 = {'method': 'update',
                     'params': ['', fake_message]}
        self.l2gw_ovsdb.responses = [self.monitor_message]

    def test_init(self):
        """Test case to test __init__."""

        fakesocket = base_test.SocketClass()
        with mock.patch.object(ovsdb_monitor.LOG, 'debug'), \
                mock.patch.object(eventlet.greenthread, 'spawn') as gt, \
                mock.patch.object(socket, 'socket',
                                  return_value=fakesocket):
            self.l2gw_ovsdb.__init__(mock.Mock(), self.conf,
                                     self.callback)
            self.assertTrue(self.l2gw_ovsdb.connected)
            self.assertTrue(gt.called)
            self.assertTrue(self.sock.called)

    def test_setup_dispatch_table(self):
        expected_dict = {'Logical_Switch':
                         self.l2gw_ovsdb._process_logical_switch,
                         'Ucast_Macs_Local':
                         self.l2gw_ovsdb._process_ucast_macs_local,
                         'Physical_Locator':
                         self.l2gw_ovsdb._process_physical_locator,
                         'Ucast_Macs_Remote':
                         self.l2gw_ovsdb._process_ucast_macs_remote,
                         'Mcast_Macs_Local':
                         self.l2gw_ovsdb._process_mcast_macs_local,
                         'Physical_Locator_Set':
                         self.l2gw_ovsdb._process_physical_locator_set
                         }
        self.l2gw_ovsdb._setup_dispatch_table()
        self.assertEqual(expected_dict, self.l2gw_ovsdb.dispatch_table)

    def test_set_monitor_response_handler(self):
        """Test case to test _set_monitor_response_handler with error_msg."""
        self.l2gw_ovsdb.connected = True
        with mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                               '_set_handler') as set_handler, \
                mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                                  'send', return_value=True) as send, \
                mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                                  '_process_response',
                                  return_value=(mock.ANY, False)
                                  ) as process_resp, \
                mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                                  '_process_monitor_msg'
                                  ) as process_monitor_msg, \
                mock.patch.object(ovsdb_monitor.LOG, 'warning'):
            self.l2gw_ovsdb.set_monitor_response_handler()
            self.assertTrue(set_handler.called)
            self.assertTrue(send.called)
            self.assertTrue(process_monitor_msg.called)
            self.assertTrue(process_resp.called)

    def test_set_monitor_response_handler_with_error_in_send(self):
        """Test case to test _set_monitor_response_handler."""
        self.l2gw_ovsdb.connected = True
        with mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                               '_set_handler') as set_handler, \
            mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                              'send', return_value=False) as send, \
            mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                              '_process_response',
                              return_value=(mock.ANY, True)) as process_resp, \
            mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                              '_process_monitor_msg') as process_monitor_msg, \
            mock.patch.object(ovsdb_monitor.LOG,
                              'warning'):
            self.l2gw_ovsdb.set_monitor_response_handler()
            self.assertTrue(set_handler.called)
            self.assertTrue(send.called)
            self.assertFalse(process_resp.called)
            self.assertFalse(process_monitor_msg.called)

    def test_update_event_handler(self):
        """Test case to test _update_event_handler."""
        with mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                               '_process_update_event'
                               ) as process_update_event:
            self.l2gw_ovsdb._update_event_handler(self.msg, mock.ANY)
            process_update_event.assert_called_once_with(self.msg, mock.ANY)

    def test_process_update_event(self):
        """Test case to test _process_update_event."""
        with mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                               '_process_physical_port'
                               ) as proc_phy_port, \
            mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                              '_process_physical_switch'
                              ) as proc_phy_switch, \
            mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                              '_process_logical_switch'
                              ) as proc_logic_switch, \
            mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                              '_process_ucast_macs_local'
                              ) as proc_ucast_mac, \
            mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                              '_process_physical_locator'
                              ) as proc_phy_loc, \
            mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                              '_process_ucast_macs_remote'
                              ) as proc_ucast_mac_remote, \
            mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                              '_process_mcast_macs_local'
                              ) as proc_mcast_mac_local, \
            mock.patch.object(
                ovsdb_monitor.OVSDBMonitor,
                '_process_physical_locator_set') as proc_phys_loc_set:
            self.l2gw_ovsdb._setup_dispatch_table()
            self.l2gw_ovsdb._process_update_event(self.msg2, mock.ANY)
            self.assertTrue(proc_phy_port.called)
            self.assertTrue(proc_phy_switch.called)
            self.assertTrue(proc_logic_switch.called)
            self.assertTrue(proc_ucast_mac.called)
            self.assertTrue(proc_phy_loc.called)
            self.assertTrue(proc_ucast_mac_remote.called)
            self.assertTrue(proc_mcast_mac_local.called)
            self.assertTrue(proc_phys_loc_set.called)
            self.assertTrue(self.callback.called)

    def test_process_response_raise_exception(self):
        """Test case to test _process_response with exception."""
        with mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                               '_response') as resp:
            with mock.patch.object(ovsdb_monitor.LOG, 'debug'):
                self.assertRaises(exceptions.OVSDBError,
                                  self.l2gw_ovsdb._process_response,
                                  self.op_id)
                resp.assert_called_once_with(self.op_id)

    def test_process_response(self):
        """Test case to test _process_response."""
        with mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                               '_response',
                               return_value={'key':
                                             'some_value'
                                             }) as resp:
            with mock.patch.object(ovsdb_monitor.LOG, 'debug'):
                self.l2gw_ovsdb._process_response(self.op_id)
                resp.assert_called_once_with(self.op_id)

    def test_default_echo_handler(self):
        """Test case to test _default_echo_handler."""
        dummy_msg = {'params': 'fake_params',
                     'id': 'fake_id'}
        with mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                               'send') as send:
            self.l2gw_ovsdb._default_echo_handler(dummy_msg, mock.ANY)
            self.assertTrue(send.called)

    def test_set_handler(self):
        """Test case to test _set_handler."""
        dummy_method = "dummy"
        dummy_handler = "handler"
        self.l2gw_ovsdb._set_handler(dummy_method, dummy_handler)
        self.assertEqual(self.l2gw_ovsdb.handlers[dummy_method],
                         dummy_handler)

    def test_on_remote_message(self):
        """Test case to test _on_remote_message."""
        self.l2gw_ovsdb.handlers = mock.Mock()
        with mock.patch.object(ovsdb_monitor.LOG, 'debug'):
            with mock.patch.object(jsonutils, 'loads') as json_load:
                self.l2gw_ovsdb._on_remote_message(self.msg)
                self.assertTrue(json_load.called)
                handler_method = json_load.return_value
                handler_method.get.assert_called_once_with(mock.ANY, mock.ANY)

    def test_rcv_thread_none(self):
        """Test case to test _rcv_thread receives None from socket."""
        self.assertTrue(self.l2gw_ovsdb.read_on)
        with mock.patch.object(self.l2gw_ovsdb.socket,
                               'recv', return_value=None) as sock_recv:
            with mock.patch.object(self.l2gw_ovsdb.socket,
                                   'close') as sock_close:
                self.l2gw_ovsdb._rcv_thread()
                self.assertTrue(sock_recv.called)
                self.assertFalse(self.l2gw_ovsdb.connected)
                self.assertFalse(self.l2gw_ovsdb.read_on)
                self.assertTrue(sock_close.called)

    def test_rcv_thread_exception(self):
        """Test case to test _rcv_thread with exception."""
        with mock.patch.object(self.l2gw_ovsdb.socket, 'recv',
                               side_effect=Exception,
                               return_value=None) as sock_recv, \
                mock.patch.object(self.l2gw_ovsdb.socket,
                                  'close') as sock_close, \
                mock.patch.object(ovsdb_monitor.LOG,
                                  'exception') as logger_call:
            self.l2gw_ovsdb._rcv_thread()
            self.assertTrue(logger_call.called)
            self.assertTrue(sock_recv.called)
            self.assertFalse(self.l2gw_ovsdb.connected)
            self.assertFalse(self.l2gw_ovsdb.read_on)
            self.assertTrue(sock_close.called)

    def test_form_ovsdb_data(self):
        some_value = mock.Mock()
        expect = {n_const.OVSDB_IDENTIFIER: self.conf.ovsdb_identifier,
                  'new_logical_switches': some_value,
                  'new_physical_switches': some_value,
                  'new_physical_ports': some_value,
                  'new_physical_locators': some_value,
                  'new_local_macs': some_value,
                  'new_remote_macs': some_value,
                  'new_mlocal_macs': some_value,
                  'new_locator_sets': some_value,
                  'deleted_logical_switches': some_value,
                  'deleted_physical_switches': some_value,
                  'deleted_physical_ports': some_value,
                  'deleted_physical_locators': some_value,
                  'deleted_local_macs': some_value,
                  'deleted_remote_macs': some_value,
                  'deleted_mlocal_macs': some_value,
                  'deleted_locator_sets': some_value,
                  'modified_logical_switches': some_value,
                  'modified_physical_switches': some_value,
                  'modified_physical_ports': some_value,
                  'modified_physical_locators': some_value,
                  'modified_local_macs': some_value,
                  'modified_remote_macs': some_value,
                  'modified_mlocal_macs': some_value,
                  'modified_locator_sets': some_value}
        with mock.patch.object(ovsdb_monitor.OVSDBMonitor,
                               '_get_list',
                               return_value=some_value
                               ):
            result = self.l2gw_ovsdb._form_ovsdb_data(mock.Mock(), mock.ANY)
            self.assertEqual(expect, result)

    def test_process_physical_port(self):
        """Test case to process new physical_port."""
        fake_id = 'fake_id'
        add = {'new': {'uuid': 'fake_id',
                       'name': 'fake_name',
                       'physical_switch_id': 'fake_switch_id',
                       'port_fault_status': 'fake_status',
                       'vlan_bindings': [["some"], []]}}
        delete = {'old': {'uuid': 'fake_id_old',
                          'name': 'fake_name_old',
                          'physical_switch_id': 'fake_switch_id_old'}}
        modify = {}
        modify.update(add)
        modify.update(delete)
        port_map = {'fake_id': 'fake_switch_id'}
        data_dict = {'new_physical_ports': [],
                     'modified_physical_ports': [],
                     'deleted_physical_ports': []}

        with mock.patch.object(ovsdb_schema, 'PhysicalPort') as phy_port:
            with mock.patch.object(ovsdb_schema, 'VlanBinding'):
                # test add
                self.l2gw_ovsdb._process_physical_port(fake_id,
                                                       add,
                                                       port_map,
                                                       data_dict)
                phy_port.assert_called_once_with(
                    fake_id, 'fake_name', None, None, 'fake_status')