"""Hero button manager (docs/plan/skin-milionar/220-hero-buttons.md): configuration, layout on the item, dispatch."""
from pathlib import Path
import sys
import unittest
from unittest.mock import patch
from urllib.parse import parse_qs, urlsplit

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE))
import stubs
stubs.install()
sys.path.insert(0, str(HERE.parent / 'skin_harness'))
import xbmc
import xbmcaddon
import xbmcgui
from wcs.skin import context, hero, hero_buttons, journal, slots
from wcs.skin.hero_contract import BASES, SECTIONS
import gen_slots

SKIN = {31902: 'Continue', 208: 'Play', 31991: 'More info'}


def buttons(item):
    return [(item.getProperty('milionar.hero.b{}l'.format(n)), item.getProperty('milionar.hero.b{}'.format(n)))
            for n in range(1, 5) if item.getProperty('milionar.hero.b{}'.format(n))]


class HeroButtonTests(unittest.TestCase):
    def setUp(self):
        self.addCleanup(patch.stopall)
        patch.object(xbmc, 'getLocalizedString', lambda number: SKIN.get(number, '')).start()
        strings = {number: 'S{}'.format(number) for number in range(32300, 32700)}
        patch.object(xbmcaddon.Addon, 'strings', strings).start()
        patch.object(xbmcaddon.Addon, 'settings', {'vinyl_user_id': '11', 'vinyl_token': 'synthetic'}).start()
        self.movie = {'ctx': 'w1', 'row': 'my_movies_all', 'type': 'movie', 'tmdb': '603', 'title': 'The Matrix'}

    def item(self, resumable=False, ids=('play', 'detail')):
        item = xbmcgui.ListItem('The Matrix')
        hero.apply(item, {'title': 'The Matrix'}, 'RunPlugin(play)', 'RunPlugin(detail)', ids=ids, resumable=resumable)
        return item

    def compose(self, item, config=None, data=None, row='my_movies_all'):
        hero_buttons.compose(item, data or self.movie, row, config, 'ENCODED')
        return buttons(item)

    def test_an_unconfigured_item_keeps_the_two_buttons_it_had(self):
        self.assertEqual(buttons(self.item()), [('Play', 'RunPlugin(play)'), ('More info', 'RunPlugin(detail)')])
        item = self.item(resumable=True)
        self.assertEqual(self.compose(item), [('Continue', 'RunPlugin(play)'), ('More info', 'RunPlugin(detail)')])
        self.assertEqual(item.getProperty('milionar.hero.b1i'), 'play')
        self.assertEqual(item.getProperty('milionar.hero.b3'), '')

    def test_defaults_are_the_former_buttons_of_every_row_kind(self):
        self.assertEqual(hero_buttons.defaults('my_movies_all')['main'], ['play', 'detail'])
        self.assertEqual(hero_buttons.defaults('my_movies_all')['more'], [])
        self.assertEqual(hero_buttons.defaults('my_series_all')['main'], ['next', 'detail'])
        self.assertEqual(hero_buttons.defaults('recent_played')['main'], ['play', 'next', 'detail'])
        self.assertEqual(hero_buttons.defaults('widget_mytv_channels')['main'], ['live', 'open'])
        self.assertEqual(hero_buttons.defaults('vinyl_curator')['main'], ['play_all', 'shuffle', 'favorite'])
        self.assertEqual(hero_buttons.defaults('vinyl_favorite_songs')['main'], ['play', 'favorite'])
        self.assertTrue(hero_buttons.defaults('vinyl_curator')['more'])
        for row in slots.TITLES:
            config = hero_buttons.defaults(row)
            self.assertEqual(sorted(sum(config.values(), [])), sorted(context.catalog(row)), row)
            self.assertLessEqual(len(config['main']) + bool(config['more']), hero_buttons.LIMIT, row)

    def test_a_series_and_a_channel_register_their_own_actions(self):
        series = self.item(ids=('next', 'detail'))
        data = dict(self.movie, type='tv', row='my_series_all')
        self.assertEqual(self.compose(series, data=data, row='my_series_all'),
                         [('Play', 'RunPlugin(play)'), ('More info', 'RunPlugin(detail)')])
        aliased = self.item(ids=('next', 'detail'))                       # only "play" configured: a series' next episode
        only_play = {'main': ['play', 'detail'], 'more': [], 'hidden': [a for a in context.catalog('my_series_all')
                                                                         if a not in ('play', 'detail')]}
        self.assertEqual(self.compose(aliased, only_play, data, 'my_series_all'),
                         [('Play', 'RunPlugin(play)'), ('More info', 'RunPlugin(detail)')])
        mixed = self.item(ids=('next', 'detail'))                         # a series in a mixed row: play does not apply
        self.assertEqual([label for label, _run in self.compose(mixed, data=data, row='recent_played')], ['Play', 'More info'])
        channel = self.item(ids=('live', 'open'))
        data = {'ctx': 'w2', 'row': 'widget_mytv_channels', 'type': 'channel', 'id': 'c1', 'kind': 'tv'}
        self.assertEqual([label for label, _run in self.compose(channel, data=data, row='widget_mytv_channels')],
                         ['Play', 'More info'])

    def test_a_channel_starts_its_broadcast_instead_of_playing(self):
        from wcs.skin import widgets
        channel = [{'id': 'c1', 'name': 'Comedy', 'media_list': [{'id': 1, 'name': 'IT Crowd'}]}]
        xbmcaddon.Addon.strings[32391] = 'Start broadcast'
        with patch.object(widgets, '_read_json', return_value=channel), patch.object(widgets, '_channel_backdrop', return_value={}):
            cinema = widgets.widget_mycinema_channels({})[0]
            with patch.object(widgets, '_Program', lambda channels: object()), patch.object(widgets, '_on_now', return_value=None):
                idle = widgets.widget_mytv_channels({})[0]
            running = {'state': {'resume_pos': 120, 'resume_total': 1300}, 's': 1, 'e': 2, 'series': 'IT Crowd'}
            with patch.object(widgets, '_Program', lambda channels: object()), patch.object(widgets, '_on_now', return_value=running):
                watching = widgets.widget_mytv_channels({})[0]
        for item in (cinema, idle):
            self.assertEqual([label for label, _run in buttons(item)], ['Start broadcast', 'More info'])
            self.assertEqual(item.getProperty('milionar.hero.b1i'), 'play')
            self.assertIn('mode=live', item.getProperty('milionar.hero.b1'))
        self.assertEqual([label for label, _run in buttons(watching)], ['Continue', 'More info'])

    def test_configured_actions_run_through_the_dispatcher_in_the_configured_order(self):
        config = {'main': ['detail', 'watched', 'play', 'season'], 'more': [], 'hidden': []}
        item = self.item()
        result = self.compose(item, config)
        self.assertEqual([label for label, _run in result], ['More info', 'S32389', 'Play'])   # season: not for a film
        self.assertEqual(result[1][1], 'RunPlugin(plugin://plugin.video.milionar/?action=skin_hero&do=watched&data=ENCODED)')
        self.assertEqual(item.getProperty('milionar.hero.b1i'), '')        # the primary button is not a play action
        self.assertEqual(item.getProperty('milionar.hero.b4'), '')

    def test_more_options_takes_the_last_place_and_the_limit_moves_the_rest(self):
        config = hero_buttons.normalize('my_movies_all', {'main': ['play', 'detail', 'watched', 'list'], 'more': ['similar']})
        self.assertEqual((config['main'], config['more'][:2]), (['play', 'detail', 'watched'], ['list', 'similar']))
        result = self.compose(self.item(), config)
        self.assertEqual([label for label, _run in result], ['Play', 'More info', 'S32389', 'S32681'])
        self.assertIn('do=more', result[3][1])
        full = hero_buttons.normalize('my_movies_all', {'main': ['play', 'detail', 'watched', 'list', 'game'], 'more': []})
        self.assertEqual((full['main'], full['more']), (['play', 'detail', 'watched'], ['list', 'game']))
        four = hero_buttons.normalize('my_movies_all', {'main': ['play', 'detail', 'watched', 'list'], 'more': [],
                                                        'hidden': [a for a in context.catalog('my_movies_all')
                                                                   if a not in ('play', 'detail', 'watched', 'list')]})
        self.assertEqual((len(four['main']), four['more']), (4, []))
        rest = [a for a in context.catalog('my_movies_all') if a not in ('play', 'season')]
        unfit = {'main': ['play'], 'more': ['season'], 'hidden': rest}        # nothing under More fits a film: no button
        self.assertEqual([label for label, _run in self.compose(self.item(), unfit)], ['Play'])

    def test_an_item_without_hero_texts_gets_no_buttons(self):
        item = xbmcgui.ListItem('Drama')
        hero_buttons.compose(item, {'type': 'folder'}, 'my_movies_genres', None, 'ENCODED')
        self.assertEqual(buttons(item), [])

    def test_availability_follows_the_item(self):
        can = hero_buttons.can
        episode = dict(self.movie, type='episode', s='1', e='2')
        self.assertTrue(can('season', episode) and can('folder', episode) and not can('collection', episode))
        self.assertTrue(can('collection', self.movie) and not can('next', self.movie))
        self.assertFalse(can('hide_continue', self.movie))
        self.assertTrue(can('hide_continue', self.movie, resumable=True))
        self.assertFalse(can('play', dict(episode, s='-1')))
        mytv = dict(episode, channel_id='c1')
        self.assertTrue(can('channel_open', mytv) and not can('watched', mytv))
        song = {'type': 'vinyl', 'kind': 'song', 'id': '9', 'ids': '9,3', 'album': 'A'}
        self.assertTrue(can('play_from', song) and can('album', song) and can('favorite', song) and not can('shuffle', song))
        genre = {'type': 'vinyl', 'kind': 'genre', 'name': 'House'}
        self.assertTrue(can('shuffle', genre) and not can('favorite', genre))
        xbmcaddon.Addon.settings.pop('vinyl_token')
        self.assertFalse(can('favorite', song))

    def test_the_dispatcher_runs_what_the_context_menu_would(self):
        available = {'watched': ('Watched', 'RunPlugin(mark)'), 'similar': ('Similar', 'RunPlugin(similar)')}
        widget = {'type': 'my_movies_all', 'hero': {'main': ['play'], 'more': ['similar', 'game'], 'hidden': []}}
        ran = []
        with patch.object(context, 'resolve', return_value=('my_movies', 2, widget, 'my_movies_all', available)), \
                patch.object(xbmc, 'executebuiltin', lambda command, *a: ran.append(command)):
            hero_buttons.run({'do': 'watched', 'data': 'ctx=w1&type=movie&tmdb=603'})
            hero_buttons.run({'do': 'collection', 'data': 'ctx=w1&type=movie&tmdb=603'})
            shown = []
            with patch.object(xbmcgui.Dialog, 'contextmenu', lambda _self, labels: shown.append(labels) or len(labels) - 1):
                hero_buttons.run({'do': 'more', 'data': 'ctx=w1&type=movie&tmdb=603'})
        self.assertEqual(ran[0], 'RunPlugin(mark)')
        self.assertEqual(xbmcgui.NOTIFICATIONS[-1], ('Milionář', 'S32696'))
        self.assertEqual(shown, [['Similar', 'S32692']])                   # game is not available; the editor closes it
        query = parse_qs(urlsplit(ran[1][len('RunPlugin('):-1]).query)
        self.assertEqual((query['action'], query['do'], query['ctx']), (['skin_hero'], ['edit'], ['w1']))

    def test_the_context_menu_offers_the_editor_only_in_hero_sections(self):
        actions = {'play': ('Play', 'p')}
        for sec, expected in (('my_movies', True), ('tmdb', False)):
            widget = {'type': 'my_movies_all', 'menu': None}
            with patch.object(context, 'find_widget', return_value=(sec, 1, widget)), \
                    patch.object(context, 'video_actions', return_value=actions):
                _top, more = context.parts({'ctx': 'w1', 'type': 'movie', 'tmdb': '1'})
            self.assertEqual(any('action=skin_hero' in command and 'do=edit' in command for _label, command in more), expected)

    def test_slot_writes_keep_the_buttons_unless_they_are_given(self):
        written = {}
        with patch.object(slots, '_set', lambda name, value: written.__setitem__(name, value) or True):
            slots.write_slot('my_movies', 1, 'my_movies_all', '20', 'added', 'poster')
            self.assertNotIn('mil.my_movies.1.hero', written)
            config = hero_buttons.defaults('my_movies_all')
            slots.write_slot('my_movies', 1, 'my_movies_all', '20', 'added', 'poster', hero=config)
            self.assertEqual(context.decode(written['mil.my_movies.1.hero']), config)
            slots.write_slot('my_movies', 1, 'off', '', '', '')
            self.assertEqual(written['mil.my_movies.1.hero'], '')

    def test_saving_buttons_reloads_every_row_of_the_section(self):
        """The buttons are on the items, also on those a genre row borrows: the section revision is in every row URL."""
        self.assertIn('hb=$INFO[Skin.String(mil.my_series.herorev)]', gen_slots.CONTENT.format(sec='my_series', n=2))
        written = {}
        widget = {'type': 'my_series_genres', 'limit': '20', 'sort': '', 'style': 'categories', 'click': '', 'uid': 'g', 'menu': None}
        with patch.object(slots, '_set', lambda name, value: written.__setitem__(name, value) or True), \
                patch.object(slots, '_refresh_slots', lambda sec, numbers: None):
            slots._write_section('my_series', [dict(widget, hero={'main': ['play'], 'more': [], 'hidden': []})], False, False)
            first = written.pop('mil.my_series.herorev')
            with patch.object(context, 'find_widget', return_value=('my_series', 3, dict(widget, hero=None))), \
                    patch('wcs.skin.context_dialogs.edit_hero', return_value=hero_buttons.defaults('my_series_genres')):
                slots.edit_saved_hero('g')
            self.assertNotIn(written['mil.my_series.herorev'], ('', first))
            written.clear()
            slots.touch_hero('tmdb')                                        # not a hero section
            self.assertEqual(written, {})

    def test_a_genre_row_lays_its_buttons_on_the_titles_it_borrows(self):
        strings = {'mil.my_series.1.type': 'my_series_next', 'mil.my_series.2.type': 'my_series_added',
                   'mil.my_series.3.type': 'my_series_genres', 'mil.my_series.3.uid': 'genres',
                   'mil.my_series.3.hero': context.encode({'main': ['play'], 'more': [], 'hidden': [
                       a for a in context.catalog('my_series_genres') if a != 'play']}),
                   'mil.my_series.2.hero': context.encode(hero_buttons.defaults('my_series_added'))}
        label = lambda name: strings.get(name[len('Skin.String('):-1], '')  # noqa: E731
        with patch.object(xbmc, 'getInfoLabel', label):
            own, lent = context.hero_config({'sec': 'my_series', 'slot': '1'})
            self.assertIsNone(own)
            self.assertEqual(list(lent), [3])                               # a title row lends nothing, slot 2 neither
            self.assertEqual(lent[3][0], 'my_series_genres')
            self.assertEqual(context.hero_config({'sec': 'tmdb', 'slot': '1'}), (False, None))
            episode = self.item(resumable=True)
            data = {'ctx': 'next', 'row': 'my_series_next', 'type': 'episode', 'tmdb': '9', 's': '4', 'e': '5'}
            context.attach('plugin://plugin.video.milionar/?action=skin_run', episode, False, 'next', 'my_series_next', own, lent)
            series = self.item(ids=('next', 'detail'))                      # "play" of a series is its next episode
            context.attach('plugin://plugin.video.milionar/?action=skin_run', series, False, 'added', 'my_series_added', None, lent)
        self.assertEqual([label for label, _run in buttons(episode)], ['Continue', 'More info'])          # its own row
        lent_buttons = lambda item: [item.getProperty('milionar.hero.s3.b{}l'.format(n)) for n in range(1, 5)]  # noqa: E731
        self.assertEqual(lent_buttons(episode), ['Continue', '', '', ''])                                # on the genre row
        self.assertEqual(lent_buttons(series), ['Play', '', '', ''])
        self.assertEqual((episode.getProperty('milionar.hero.s3'), episode.getProperty('milionar.hero.s3.b1i')), ('1', 'play'))
        self.assertEqual(episode.getProperty('milionar.hero.s2'), '')

    def test_a_lent_button_belongs_to_the_lending_rows_widget(self):
        item = self.item()
        config = {'main': ['watched'], 'more': ['similar'], 'hidden': [a for a in context.catalog('my_movies_genres')
                                                                       if a not in ('watched', 'similar')]}
        hero_buttons.compose(item, self.movie, 'my_movies_genres', config, 'ENCODED', 's3.', 'genres')
        self.assertTrue(item.getProperty('milionar.hero.s3.b1').endswith('do=watched&data=ENCODED&owner=genres)'))
        self.assertTrue(item.getProperty('milionar.hero.s3.b2').endswith('do=more&data=ENCODED&owner=genres)'))
        widget = {'type': 'my_movies_genres', 'hero': config}
        ran, shown = [], []
        with patch.object(context, 'resolve', return_value=('my_movies', 1, {'type': 'my_movies_all', 'hero': None},
                                                            'my_movies_all', {'similar': ('Similar', 's'), 'game': ('Game', 'g')})), \
                patch.object(context, 'find_widget', return_value=('my_movies', 3, widget)), \
                patch.object(xbmc, 'executebuiltin', lambda command, *a: ran.append(command)), \
                patch.object(xbmcgui.Dialog, 'contextmenu', lambda _self, labels: shown.append(labels) or len(labels) - 1):
            hero_buttons.run({'do': 'more', 'data': 'ctx=w1&type=movie&tmdb=603', 'owner': 'genres'})
        self.assertEqual(shown, [['Similar', 'S32692']])                   # the genre widget's More, not the lender's
        self.assertIn('ctx=genres', ran[0])

    def test_the_editor_of_a_focused_hero_button_is_the_current_rows(self):
        labels = {'Window(Home).Property(milionar.row.my_series)': '30300',
                  'Container(30300).ListItem.Property(milionar.ctxdata)': 'ctx%3Dgenres%26type%3Dfolder',
                  'Container(30100).ListItem.Property(milionar.hero.title)': 'IT Crowd',
                  'Container(30100).ListItem.Property(milionar.ctxdata)': 'ctx%3Dnext%26type%3Depisode'}
        with patch.object(xbmc, 'getInfoLabel', lambda name: labels.get(name, '')):
            self.assertEqual(hero_buttons.focused_widget('30901'), 'genres')
            self.assertEqual(hero_buttons.focused_widget('30906'), 'genres')
            labels.pop('Window(Home).Property(milionar.row.my_series)')
            self.assertEqual(hero_buttons.focused_widget('30904'), 'next')       # no current row: the first with titles
            self.assertEqual(hero_buttons.focused_widget('30100'), '')

    def test_a_prepared_row_is_rebuilt_when_the_buttons_change(self):
        params = {'action': 'widget_row', 'row': 'cont_movies', 'sec': 'cont', 'slot': '1', 'hb': 'one'}
        self.assertNotEqual(journal.name(journal.normalize(params)), journal.name(journal.normalize(dict(params, hb='two'))))

    def test_the_contract_matches_the_generated_sections(self):
        self.assertEqual(BASES, {sec: gen_slots.SECTIONS[sec] for sec in SECTIONS})


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