# Copyright 2023 Cloudbase Solutions Srl
# All Rights Reserved.

import logging
import os
from unittest import mock

import ddt

from coriolis import service
from coriolis.tests import test_base


@ddt.ddt
class ServiceTestCase(test_base.CoriolisBaseTestCase):
    """Test suite for the Coriolis Service class."""

    @ddt.data(
        (['--worker-process-count', '5'], 5, [], False),
        (['--worker-process-count', '10'], 10, [], False),
        (['--worker-process-count', '15'], 15, [], False),
        (['--worker-process-count', '20'], 20, [], False),
        ([], None, [], False),
        (['--worker-process-count', '5', '--unknown-arg'], 5,
         ['--unknown-arg'], False),
        (['--worker-process-count', '-5'], None, None, True),
        (['--worker-process-count', '0'], None, None, True),
        (['--worker-process-count', 'not-a-number'], None, None, True)
    )
    @ddt.unpack
    def test_get_worker_count_from_args(self, input_args, expected_count,
                                        expected_unknown_args,
                                        expect_exception):
        if expect_exception:
            self.assertRaises(SystemExit, service.get_worker_count_from_args,
                              input_args)
        else:
            worker_count, unknown_args = service.get_worker_count_from_args(
                input_args)
            self.assertEqual(worker_count, expected_count)
            self.assertEqual(unknown_args, expected_unknown_args)

    @ddt.data(
        ({}, False, False, [], logging.WARN),
        ({'lock_path': ""}, False, False, [], logging.WARN),
        ({'lock_path': "/path/to/locks"}, False, False, [], logging.WARN),
        ({'lock_path': "/path/to/locks"}, True, False, [], logging.WARN),
        ({'lock_path': "/path/to/locks"}, True, True, ['file1', 'file2'],
         logging.WARN),
        ({'lock_path': "/path/to/locks"}, True, True, [], logging.INFO),
    )
    @ddt.unpack
    @mock.patch.object(os.path, 'exists')
    @mock.patch.object(os.path, 'isdir')
    @mock.patch.object(os, 'listdir')
    def test_check_locks_dir_empty(self, oslo_concurrency, exists, isdir,
                                   listdir, expected_log_level, mock_listdir,
                                   mock_isdir, mock_exists):
        mock_exists.return_value = exists
        mock_isdir.return_value = isdir
        mock_listdir.return_value = listdir
        with mock.patch.object(service.CONF, 'oslo_concurrency',
                               oslo_concurrency):
            with self.assertLogs('coriolis.service', level=expected_log_level):
                service.check_locks_dir_empty()


class WSGIServiceTestCase(test_base.CoriolisBaseTestCase):
    """Test suite for the Coriolis WSGIService class."""

    @mock.patch('oslo_service.wsgi.Loader.load_app')
    @mock.patch.object(service, 'rpc')
    @mock.patch.object(service, 'CONF')
    @mock.patch.object(service.wsgi, 'Server')
    def test_init(self, mock_server, mock_conf, mock_rpc, mock_load_app):
        mock_conf.api_migration_listen = mock.sentinel.listen
        mock_conf.api_migration_listen_port = mock.sentinel.listen_port
        mock_conf.api_migration_workers = mock.sentinel.workers
        mock_conf.api_migration_listen = mock.sentinel.listen
        mock_rpc.init.return_value = mock.sentinel.transport

        wsgi_service = service.WSGIService(mock.sentinel.name, worker_count=10,
                                           init_rpc=True)

        mock_rpc.init.assert_called_once_with()
        mock_load_app.assert_called_once_with(mock.sentinel.name)
        mock_server.assert_called_once_with(
            server_name=mock.sentinel.name,
            wsgi_app=mock_load_app.return_value,
            bind_addr=(
                mock_conf.api_migration_listen,
                mock_conf.api_migration_listen_port),
        )
        self.assertEqual(wsgi_service._host, mock_conf.api_migration_listen)
        self.assertEqual(wsgi_service._port,
                         mock_conf.api_migration_listen_port)
        self.assertEqual(wsgi_service._workers, 10)
        self.assertEqual(wsgi_service._app, mock_load_app.return_value)
        self.assertEqual(wsgi_service._server, mock_server.return_value)

    @mock.patch('platform.system')
    @mock.patch('oslo_service.wsgi.Server')
    @mock.patch('oslo_service.wsgi.Loader.load_app')
    @mock.patch('coriolis.rpc.messaging.get_transport')
    @mock.patch.object(service, 'CONF')
    def test_init_windows(self, mock_conf, mock_get_transport, mock_load_app,
                          mock_server, mock_system):
        mock_system.return_value = "Windows"
        mock_conf.api_migration_workers = None
        mock_load_app.return_value = mock.MagicMock()
        mock_server.return_value = mock.MagicMock()
        mock_get_transport.return_value = mock.MagicMock()

        result = service.WSGIService('test_service', None, True)

        self.assertEqual(result._workers, 1)

    @mock.patch('platform.system')
    @mock.patch('oslo_service.wsgi.Server')
    @mock.patch('oslo_service.wsgi.Loader.load_app')
    @mock.patch('oslo_concurrency.processutils.get_worker_count')
    @mock.patch('coriolis.rpc.messaging.get_transport')
    @mock.patch.object(service, 'CONF')
    def test_init_sets_workers_based_on_system_on_non_windows(
            self, mock_conf, mock_get_transport, mock_get_worker_count,
            mock_load_app, mock_server, mock_system):
        mock_system.return_value = "Linux"
        mock_conf.api_migration_workers = None
        mock_load_app.return_value = mock.MagicMock()
        mock_server.return_value = mock.MagicMock()
        mock_get_transport.return_value = mock.MagicMock()

        result = service.WSGIService('test_service', None, True)

        self.assertEqual(result._workers, mock_get_worker_count.return_value)

    @mock.patch.object(service, 'CONF')
    @mock.patch('oslo_service.wsgi.Loader.load_app')
    @mock.patch('coriolis.rpc.messaging.get_transport')
    def test_service_methods(self, mock_get_transport,
                             mock_load_app, mock_conf):
        mock_conf.api_migration_workers = 10
        mock_load_app.return_value = mock.MagicMock()
        mock_get_transport.return_value = mock.MagicMock()

        result = service.WSGIService('test_service', None, True)

        result._server = mock.MagicMock()

        self.assertEqual(result.get_workers_count(),
                         mock_conf.api_migration_workers)

        result.start()
        result._server.serve.assert_called_once()

        result.stop()
        result._server.stop.assert_called_once()

        result.wait()

        # no-op, no greenpool to adjust.
        result.reset()


class MessagingServiceTestCase(test_base.CoriolisBaseTestCase):
    """Test suite for the Coriolis MessagingService class."""

    @mock.patch.object(service, 'rpc')
    @mock.patch.object(service, 'CONF')
    @mock.patch.object(service.messaging, 'Target')
    @mock.patch.object(service.utils, 'get_hostname')
    def test_init(self, mock_get_hostname, mock_target, mock_conf, mock_rpc):
        mock_conf.api_migration_listen = mock.sentinel.listen
        mock_conf.api_migration_listen_port = mock.sentinel.listen_port
        mock_conf.api_migration_workers = mock.sentinel.workers
        mock_conf.api_migration_listen = mock.sentinel.listen
        mock_rpc.init.return_value = mock.sentinel.transport
        mock_get_hostname.return_value = mock.sentinel.hostname

        messaging_service = service.MessagingService(
            mock.sentinel.topic, mock.sentinel.endpoints,
            mock.sentinel.version, worker_count=10, init_rpc=True)

        mock_rpc.init.assert_called_once_with()
        mock_target.assert_called_once_with(
            topic=mock.sentinel.topic,
            server=mock.sentinel.hostname,
            version=mock.sentinel.version)
        mock_rpc.get_server.assert_called_once_with(
            mock_target.return_value, mock.sentinel.endpoints)
        self.assertEqual(messaging_service._workers, 10)
        self.assertEqual(messaging_service._server,
                         mock_rpc.get_server.return_value)

    @mock.patch('platform.system')
    @mock.patch.object(service, 'rpc')
    @mock.patch.object(service, 'CONF')
    @mock.patch.object(service.messaging, 'Target')
    @mock.patch.object(service.utils, 'get_hostname')
    def test_init_windows(self, mock_get_hostname, mock_target, mock_conf,
                          mock_rpc, mock_system):
        mock_system.return_value = "Windows"
        mock_conf.api_migration_listen = mock.sentinel.listen
        mock_conf.api_migration_listen_port = mock.sentinel.listen_port
        mock_conf.api_migration_workers = mock.sentinel.workers
        mock_conf.api_migration_listen = mock.sentinel.listen
        mock_rpc.init.return_value = mock.sentinel.transport
        mock_get_hostname.return_value = mock.sentinel.hostname

        messaging_service = service.MessagingService(
            mock.sentinel.topic, mock.sentinel.endpoints,
            mock.sentinel.version, worker_count=10, init_rpc=True)

        mock_rpc.init.assert_called_once_with()
        mock_target.assert_called_once_with(
            topic=mock.sentinel.topic,
            server=mock.sentinel.hostname,
            version=mock.sentinel.version)
        mock_rpc.get_server.assert_called_once_with(
            mock_target.return_value, mock.sentinel.endpoints)
        self.assertEqual(messaging_service._workers, 1)
        self.assertEqual(messaging_service._server,
                         mock_rpc.get_server.return_value)

    @mock.patch.object(service, 'rpc')
    @mock.patch.object(service, 'CONF')
    @mock.patch.object(service.utils, 'get_hostname')
    @mock.patch.object(service.processutils, 'get_worker_count')
    def test_init_with_none_worker_count(self, mock_get_worker_count,
                                         mock_get_hostname,
                                         mock_conf, mock_rpc):
        mock_conf.api_migration_listen = mock.sentinel.listen
        mock_conf.api_migration_listen_port = mock.sentinel.listen_port
        mock_conf.api_migration_listen = mock.sentinel.listen
        mock_rpc.init.return_value = mock.sentinel.transport
        mock_get_hostname.return_value = mock.sentinel.hostname

        result = service.MessagingService(
            mock.sentinel.topic, mock.sentinel.endpoints,
            mock.sentinel.version, worker_count=None, init_rpc=True)

        self.assertEqual(result._workers, mock_get_worker_count.return_value)

    @mock.patch.object(service, 'rpc')
    @mock.patch.object(service, 'CONF')
    @mock.patch.object(service.utils, 'get_hostname')
    @mock.patch.object(service.processutils, 'get_worker_count')
    def test_service_methods(self, mock_get_worker_count, mock_get_hostname,
                             mock_conf, mock_rpc):
        mock_conf.api_migration_listen = mock.sentinel.listen
        mock_conf.api_migration_listen_port = mock.sentinel.listen_port
        mock_conf.api_migration_workers = mock.sentinel.worker_count
        mock_conf.api_migration_listen = mock.sentinel.listen
        mock_rpc.init.return_value = mock.sentinel.transport
        mock_get_hostname.return_value = mock.sentinel.hostname
        mock_get_worker_count.return_value = mock.sentinel.worker_count

        mock_server = mock.MagicMock()
        mock_rpc.get_server.return_value = mock_server

        result = service.MessagingService(
            mock.sentinel.topic, mock.sentinel.endpoints,
            mock.sentinel.version, worker_count=None, init_rpc=True)

        self.assertEqual(result.get_workers_count(),
                         mock.sentinel.worker_count)

        result.start()
        mock_server.start.assert_called_once()

        result.stop()
        mock_server.stop.assert_called_once()

        result.wait()

        result.reset()
        mock_server.reset.assert_called_once()
