# -*- encoding: utf8 -*-
#
# The Qubes OS Project, http://www.qubes-os.org
#
# Copyright (C) 2017 Marek Marczykowski-Górecki
#                               <marmarek@invisiblethingslab.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License along
# with this program; if not, see <http://www.gnu.org/licenses/>.



import socket
import subprocess
import qubesadmin.tests
import unittest
try:
    # qubesadmin.events require python3, so this tests can also use python3 features
    import asyncio
    import unittest.mock
    import qubesadmin.events
except ImportError:
    # don't run any tests on python2
    def load_tests(loader, tests, pattern):
        return unittest.TestSuite()
    # don't fail on coroutine decorator
    class asyncio(object):
        @staticmethod
        def coroutine(f):
            return f


class TC_00_Events(qubesadmin.tests.QubesTestCase):
    def setUp(self):
        super().setUp()
        self.app = unittest.mock.MagicMock()
        self.dispatcher = qubesadmin.events.EventsDispatcher(self.app)

    def test_000_handler_specific(self):
        handler = unittest.mock.Mock()
        self.dispatcher.add_handler('some-event', handler)
        self.dispatcher.handle('', 'some-event', arg1='value1')
        handler.assert_called_once_with(None, 'some-event', arg1='value1')
        handler.reset_mock()
        self.dispatcher.handle('test-vm', 'some-event', arg1='value1')
        handler.assert_called_once_with(
            self.app.domains.get_blind('test-vm'), 'some-event', arg1='value1')
        handler.reset_mock()
        self.dispatcher.handle('', 'other-event', arg1='value1')
        self.assertFalse(handler.called)
        self.dispatcher.remove_handler('some-event', handler)
        self.dispatcher.handle('', 'some-event', arg1='value1')
        self.assertFalse(handler.called)

    def test_001_handler_glob(self):
        handler = unittest.mock.Mock()
        self.dispatcher.add_handler('*', handler)
        self.dispatcher.handle('', 'some-event', arg1='value1')
        handler.assert_called_once_with(None, 'some-event', arg1='value1')
        handler.reset_mock()
        self.dispatcher.handle('test-vm', 'some-event', arg1='value1')
        handler.assert_called_once_with(
            self.app.domains.get_blind('test-vm'), 'some-event', arg1='value1')
        handler.reset_mock()
        self.dispatcher.handle('', 'other-event', arg1='value1')
        handler.assert_called_once_with(None, 'other-event', arg1='value1')
        handler.reset_mock()
        self.dispatcher.remove_handler('*', handler)
        self.dispatcher.handle('', 'some-event', arg1='value1')
        self.assertFalse(handler.called)

    def test_002_handler_glob_partial(self):
        handler = unittest.mock.Mock()
        self.dispatcher.add_handler('some-*', handler)
        self.dispatcher.handle('', 'some-event', arg1='value1')
        handler.assert_called_once_with(None, 'some-event', arg1='value1')
        handler.reset_mock()
        self.dispatcher.handle('test-vm', 'some-event', arg1='value1')
        handler.assert_called_once_with(
            self.app.domains.get_blind('test-vm'), 'some-event', arg1='value1')
        handler.reset_mock()
        self.dispatcher.handle('', 'other-event', arg1='value1')
        self.assertFalse(handler.called)
        handler.reset_mock()
        self.dispatcher.remove_handler('some-*', handler)
        self.dispatcher.handle('', 'some-event', arg1='value1')
        self.assertFalse(handler.called)

    @asyncio.coroutine
    def mock_get_events_reader(self, stream, cleanup_func, expected_vm,
            vm=None):
        self.assertEqual(expected_vm, vm)
        return stream, cleanup_func

    @asyncio.coroutine
    def send_events(self, stream, events):
        for event in events:
            stream.feed_data(event)
            # don't use yield from...
            sleep = asyncio.sleep(0.01)
            for x in iter(lambda: sleep.send(None), None):
                yield x
        stream.feed_eof()

    def test_010_listen_for_events(self):
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        stream = asyncio.StreamReader()
        cleanup_func = unittest.mock.Mock()
        self.dispatcher._get_events_reader = \
            lambda vm: self.mock_get_events_reader(stream, cleanup_func,
                None, vm)
        handler = unittest.mock.Mock()
        self.dispatcher.add_handler('some-event', handler)
        events = [
            b'1\0\0some-event\0arg1\0value1\0\0',
            b'1\0some-vm\0some-event\0arg1\0value1\0\0',
            b'1\0some-vm\0some-event\0arg_without_value\0\0arg2\0value\0\0',
            b'1\0some-vm\0other-event\0\0',
        ]
        asyncio.ensure_future(self.send_events(stream, events))
        loop.run_until_complete(self.dispatcher.listen_for_events(
            reconnect=False))
        self.assertEqual(handler.mock_calls, [
            unittest.mock.call(None, 'some-event', arg1='value1'),
            unittest.mock.call(
                self.app.domains.get_blind('some-vm'), 'some-event',
                arg1='value1'),
            unittest.mock.call(
                self.app.domains.get_blind('some-vm'), 'some-event',
                arg_without_value='', arg2='value'),
        ])
        cleanup_func.assert_called_once_with()
        loop.close()

    def mock_open_unix_connection(self, expected_path, sock, path):
        self.assertEqual(expected_path, path)
        return asyncio.open_connection(sock=sock)

    def read_all(self, sock):
        buf = b''
        for data in iter(lambda: sock.recv(4096), b''):
            buf += data
        return buf

    def test_020_get_events_reader_local(self):
        self.app.qubesd_connection_type = 'socket'
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        sock1, sock2 = socket.socketpair()
        with unittest.mock.patch('asyncio.open_unix_connection',
                lambda path: self.mock_open_unix_connection(
                    qubesadmin.config.QUBESD_SOCKET, sock1, path)):
            task = asyncio.ensure_future(self.dispatcher._get_events_reader())
            reader = asyncio.ensure_future(loop.run_in_executor(None,
                self.read_all, sock2))
            loop.run_until_complete(asyncio.wait([task, reader]))
            self.assertEqual(reader.result(),
                b'dom0\0admin.Events\0dom0\0\0')
            self.assertIsInstance(task.result()[0], asyncio.StreamReader)
            cleanup_func = task.result()[1]
            cleanup_func()
            sock2.close()

        # run socket cleanup functions
        loop.stop()
        loop.run_forever()
        loop.close()

    def test_021_get_events_reader_local_vm(self):
        self.app.qubesd_connection_type = 'socket'
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        sock1, sock2 = socket.socketpair()
        vm = unittest.mock.Mock()
        vm.name = 'test-vm'
        with unittest.mock.patch('asyncio.open_unix_connection',
                lambda path: self.mock_open_unix_connection(
                    qubesadmin.config.QUBESD_SOCKET, sock1, path)):
            task = asyncio.ensure_future(self.dispatcher._get_events_reader(vm))
            reader = asyncio.ensure_future(loop.run_in_executor(None,
                self.read_all, sock2))
            loop.run_until_complete(asyncio.wait([task, reader]))
            self.assertEqual(reader.result(),
                b'dom0\0admin.Events\0test-vm\0\0')
            self.assertIsInstance(task.result()[0], asyncio.StreamReader)
            cleanup_func = task.result()[1]
            cleanup_func()
            sock2.close()

        # run socket cleanup functions
        loop.stop()
        loop.run_forever()
        loop.close()

    @asyncio.coroutine
    def mock_coroutine(self, mock, *args, **kwargs):
        return mock(*args, **kwargs)

    def test_022_get_events_reader_remote(self):
        self.app.qubesd_connection_type = 'qrexec'
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        mock_proc = unittest.mock.Mock()
        with unittest.mock.patch('asyncio.create_subprocess_exec',
                lambda *args, **kwargs: self.mock_coroutine(mock_proc,
                    *args, **kwargs)):
            task = asyncio.ensure_future(self.dispatcher._get_events_reader())
            loop.run_until_complete(task)
            self.assertEqual(mock_proc.mock_calls, [
                unittest.mock.call('qrexec-client-vm', 'dom0',
                    'admin.Events', stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE),
                unittest.mock.call().stdin.write_eof()
            ])
            self.assertEqual(task.result()[0], mock_proc().stdout)
            cleanup_func = task.result()[1]
            cleanup_func()
            unittest.mock.call().kill.assert_called_once_with()

        loop.close()

    def test_023_get_events_reader_remote_vm(self):
        self.app.qubesd_connection_type = 'qrexec'
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        mock_proc = unittest.mock.Mock()
        vm = unittest.mock.Mock()
        vm.name = 'test-vm'
        with unittest.mock.patch('asyncio.create_subprocess_exec',
                lambda *args, **kwargs: self.mock_coroutine(mock_proc,
                    *args, **kwargs)):
            task = asyncio.ensure_future(self.dispatcher._get_events_reader(vm))
            loop.run_until_complete(task)
            self.assertEqual(mock_proc.mock_calls, [
                unittest.mock.call('qrexec-client-vm', 'test-vm',
                    'admin.Events', stdin=subprocess.PIPE,
                    stdout=subprocess.PIPE),
                unittest.mock.call().stdin.write_eof()
            ])
            self.assertEqual(task.result()[0], mock_proc().stdout)
            cleanup_func = task.result()[1]
            cleanup_func()
            unittest.mock.call().kill.assert_called_once_with()

        loop.close()

    def test_030_events_device(self):
        handler = unittest.mock.Mock()
        self.dispatcher.add_handler('device-attach:test', handler)
        self.dispatcher.handle('test-vm', 'device-attach:test',
            device='test-vm2:dev', options='{}')
        vm = self.app.domains.get_blind('test-vm')
        dev = self.app.domains.get_blind('test-vm2').devices['test']['dev']
        handler.assert_called_once_with(vm, 'device-attach:test', device=dev,
            options='{}')
