#!/usr/bin/env python3
"""Offline checks of the context menu without a plugin start (wcs/skin/commands.py, wcs.skin.context.run_chosen).

Run: python3 scripts/skin_harness/test_commands.py
"""
from pathlib import Path
import re
import sys
import time
import unittest
from unittest.mock import patch

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'scripts/library_harness'))
import stubs
stubs.install()
import xbmc
from wcs.skin import commands, context, local


class CommandTests(unittest.TestCase):
    def wait(self, calls):
        deadline = time.time() + 2
        while not calls and time.time() < deadline:
            time.sleep(0.01)

    def test_the_keymap_and_the_native_menu_ask_the_service_and_it_knows_every_request(self):
        keymap = (ROOT / 'resources/keymaps/milionar-home.xml').read_text(encoding='utf-8')
        native = (ROOT / 'resources/skin.milionar/xml/DialogContextMenu.xml').read_text(encoding='utf-8')
        asked = set(re.findall(r'NotifyAll\(plugin\.video\.milionar,(\w+)\)', keymap + native))
        self.assertEqual(asked, set(commands.METHODS))
        self.assertNotIn('RunPlugin', keymap)                              # no plugin start for a menu
        self.assertNotIn('action=skin_context', native)
        for function in commands.METHODS.values():
            self.assertTrue(callable(getattr(context, function)))
        for method in commands.METHODS:                                    # the plugin actions stay for other callers
            self.assertTrue(local.handles(method))

    def test_a_request_runs_its_function_in_a_thread_and_unknown_ones_are_left_alone(self):
        calls = []
        with patch.object(context, 'home_context', lambda: calls.append('home')):
            self.assertTrue(commands.handle('skin_context'))
            self.wait(calls)
        self.assertEqual(calls, ['home'])
        self.assertFalse(commands.handle('skin_rows_changed'))

    def test_the_listener_passes_the_notification_on(self):
        with patch.object(commands, 'handle') as handle:
            monitor = commands._Monitor()
            monitor.onNotification('plugin.video.milionar', 'Other.skin_context', '')
            monitor.onNotification('another.addon', 'Other.skin_context', '')
        handle.assert_called_once_with('skin_context')

    def test_a_failing_menu_does_not_block_the_next_one(self):
        calls = []

        def broken():
            calls.append('x')
            raise RuntimeError('broken menu')
        with patch.object(context, 'home_context', broken):
            commands._run('skin_context')
            commands._run('skin_context')
        self.assertEqual(calls, ['x', 'x'])

    def test_quick_actions_run_here_and_everything_else_is_the_builtin(self):
        from wcs.library2 import ctx, play
        from wcs.skin import slots
        base = 'RunPlugin(plugin://plugin.video.milionar/?'
        with patch.object(xbmc, 'executebuiltin') as builtin, patch.object(ctx, 'run') as run, \
                patch.object(play, 'request') as request, patch.object(slots, 'run') as manager:
            context.run_chosen(base + 'action=ctx_mark&type=movie&tmdb=5&watched=1)')
            run.assert_called_once_with({'action': 'ctx_mark', 'type': 'movie', 'tmdb': '5', 'watched': '1'})
            context.run_chosen(base + 'action=mplay_req&type=movie&tmdb=5&src=known&from=resume)')
            self.assertEqual(request.call_args.args[1:], ('known', 'resume'))
            context.run_chosen(base + 'action=skin_wm&op=edit)')
            manager.assert_called_once_with({'action': 'skin_wm', 'op': 'edit'})
            builtin.assert_not_called()
            # a big dialog of the addon, a listing, another addon, any other builtin: as the builtin it is
            for command in (base + 'action=my_tv_dialog)', base + 'action=milionar_demo&id=1)',
                            'ActivateWindow(Videos,plugin://plugin.video.milionar/?action=mlist,return)',
                            'RunPlugin(plugin://plugin.video.other/?action=ctx_mark)', 'SetFocus(29905)'):
                context.run_chosen(command)
                self.assertEqual(builtin.call_args.args[0], command)

    def test_an_action_that_fails_here_is_given_to_the_plugin(self):
        from wcs.library2 import ctx
        command = 'RunPlugin(plugin://plugin.video.milionar/?action=ctx_list&type=movie&tmdb=5&add=1)'
        with patch.object(xbmc, 'executebuiltin') as builtin, patch.object(ctx, 'run', side_effect=RuntimeError('x')):
            context.run_chosen(command)
        builtin.assert_called_once_with(command)

    def test_details_open_here_only_in_our_skin(self):
        from wcs.skin import detail
        command = 'RunPlugin(plugin://plugin.video.milionar/?action=show_wcs_details&media_type=movie&tmdb_id=5)'
        with patch.object(xbmc, 'executebuiltin') as builtin, patch.object(detail, 'open_detail') as opened:
            context.run_chosen(command)
            opened.assert_called_once()
            builtin.assert_not_called()
            with patch.object(xbmc, 'getSkinDir', return_value='skin.estuary'):
                context.run_chosen(command)
            builtin.assert_called_once_with(command)

    def test_listings_and_big_dialogs_are_not_quick_actions(self):
        for action in local.LISTINGS + ('widget_row', 'mlist', 'mplay', 'skin_run', 'my_tv_dialog', 'milionar_demo', 'library_migrate', ''):
            self.assertFalse(local.handles(action), action)
        for action in ('ctx_mark', 'skin_wm', 'skin_detail_back', 'vinyl_item', 'mplay_req', 'skin_hero', 'foryou_rate'):
            self.assertTrue(local.handles(action), action)

    def test_the_plugin_call_does_a_quick_action_the_same_one_way(self):
        main = (ROOT / 'main.py').read_text(encoding='utf-8')
        self.assertIn('if local.run(_params, int(sys.argv[1])):', main)
        for action in local.ACTIONS:                                       # no second copy of a quick action in main.py
            self.assertNotIn("_action == '{}'".format(action), main.replace("_action == 'show_wcs_details'", ''))

    def test_the_skin_sends_a_builtin_to_the_service_and_runs_it_itself_without_it(self):
        sys.path.insert(0, str(ROOT / 'scripts/skin_harness'))
        import gen_slots
        self.assertEqual(gen_slots.RUN.format('X'), 'NotifyAll(plugin.video.milionar,{}X)'.format(commands.RUN))
        self.assertIn('Window(Home).Property({})'.format(commands.ALIVE), gen_slots.ALIVE)
        calls = []
        with patch.object(context, 'run_chosen', lambda command: calls.append(command)):
            self.assertTrue(commands.handle('run:ActivateWindow(Videos,plugin://x/?a=1&b=2,return)'))
            self.wait(calls)
        self.assertEqual(calls, ['ActivateWindow(Videos,plugin://x/?a=1&b=2,return)'])
        # no hand-written skin file starts the plugin for a quick action on a click any more
        pattern = re.compile(r'<(onclick|onback|oninfo|onfocus)>RunPlugin\(plugin://plugin\.video\.milionar/\?action=([a-z_]+)')
        for path in sorted((ROOT / 'resources/skin.milionar/xml').glob('*.xml')):
            for _tag, action in pattern.findall(path.read_text(encoding='utf-8')):
                self.assertFalse(local.handles(action), '{}: {}'.format(path.name, action))


if __name__ == '__main__':
    unittest.main()
