# -*- coding: utf-8 -*-
"""L4 unit tests: Home slot defaults and the slot editor (docs/plan/milionar-library/070 §2, §5)."""
import os
import re
import sys
import unittest

HERE = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, HERE)
import stubs  # noqa: E402

stubs.install()

import xbmc  # noqa: E402
import xbmcaddon  # noqa: E402
import xbmcgui  # noqa: E402

from wcs.library2 import rows  # noqa: E402
from wcs.skin import defaults as skin_defaults, slots  # noqa: E402


class SlotTests(unittest.TestCase):
    def setUp(self):
        self.commands, self.strings = [], {}
        skin_defaults._cache[:] = [{}]                         # without the shipped skin.json: the built-in SECTIONS defaults
        self._sections = dict(slots.SECTIONS)             # the manager tests use the original three-widget layouts
        for sec, defaults in (('cont', [('cont_movies', '20', 'activity', 'landscape'), ('cont_series', '20', 'activity', 'episode'),
                                        ('recent_played', '20', 'time', 'poster')]),
                              ('my_movies', [('my_movies_progress', '20', 'activity', 'landscape'), ('my_movies_added', '20', 'added', 'poster'),
                                             ('my_movies_genres', '20', '', 'categories')]),
                              ('my_series', [('my_series_next', '20', 'activity', 'episode'), ('my_series_new', '20', 'air', 'episode'),
                                             ('my_series_genres', '20', '', 'categories')])):
            slots.SECTIONS[sec] = slots.SECTIONS[sec][:2] + (defaults,)
        self._exec, self._info = xbmc.executebuiltin, xbmc.getInfoLabel
        xbmc.executebuiltin = self.fake_exec
        self.info = {}
        xbmc.getInfoLabel = lambda label: (self.strings.get(re.match(r'Skin\.String\((.*)\)', label).group(1), '')
                                           if label.startswith('Skin.String(') else self.info.get(label, ''))
        xbmcgui.Window.props.clear()
        xbmcaddon.Addon.strings.update({code: 'T{}'.format(code) for code in range(32300, 32400)})
        xbmcaddon.Addon.strings[32600] = 'Context menu'
        self.selected = []
        self._select = xbmcgui.Dialog.select
        xbmcgui.Dialog.select = lambda dialog, heading, labels, *a, **k: self.selected.pop(0) if self.selected else -1

    def tearDown(self):
        skin_defaults._cache[:] = []
        slots.SECTIONS.clear()
        slots.SECTIONS.update(self._sections)
        xbmc.executebuiltin, xbmc.getInfoLabel = self._exec, self._info
        xbmcgui.Dialog.select = self._select

    def fake_exec(self, command, wait=False):
        self.commands.append(command)
        match = re.match(r'Skin\.SetString\((.*?),"(.*)"\)$', command)
        if match:
            self.strings[match.group(1)] = match.group(2)
        reset = re.match(r'Skin\.Reset\((.*)\)$', command)
        if reset:
            self.strings.pop(reset.group(1), None)

    def test_defaults_cover_every_section_and_match_the_plan(self):
        slots.apply_defaults()
        self.assertIn('Skin.SetBool(MilionarSlotsV1)', self.commands)
        self.assertEqual([self.strings['mil.my_movies.{}.type'.format(n)] for n in (1, 2, 3, 4)],
                         ['my_movies_progress', 'my_movies_added', 'my_movies_genres', 'off'])          # max 3 rows by default
        self.assertEqual([self.strings['mil.my_series.{}.type'.format(n)] for n in (1, 2, 3, 4)],
                         ['my_series_next', 'my_series_new', 'my_series_genres', 'off'])
        for sec in slots.SECTIONS:
            self.assertLessEqual(len(slots.SECTIONS[sec][2]), slots.SLOTS, sec)
        self.assertEqual(self.strings['mil.my_movies.1.style'], 'landscape')
        self.assertEqual(self.strings['mil.cont.2.type'], 'cont_series')
        self.assertEqual(self.strings['mil.cont.upcoming'], '1')                      # decision 10: on by default
        self.assertEqual(self.strings['mil.my_series.upcoming'], '1')
        self.assertNotIn('mil.my_movies.upcoming', self.strings)
        self.assertEqual(self.strings['mil.mytv.2.type'], 'widget_mytv_next')
        self.assertEqual(self.strings['mil.tmdb.3.style'], 'categories')
        self.assertEqual(self.strings['mil.my_movies.5.type'], 'off')
        self.assertNotIn('mil.my_movies.5.limit', self.strings)
        self.assertEqual(self.strings['mil.my_movies.1.title'], 'T32300')

    def test_every_default_row_has_a_title_and_a_valid_style(self):
        for sec, (_name, allowed, defaults) in slots.SECTIONS.items():
            for row, limit, sort, style in defaults:
                self.assertIn(row, allowed)
                self.assertIn(row, slots.TITLES)
                self.assertIn(style, slots.styles_of(row), row)
                self.assertIn(limit, slots.LIMITS)
                if sort:
                    self.assertIn(sort, slots.sorts_of(row) or (sort,), row)
        for row in rows.CATALOGUE:                                                      # every catalogue row is selectable
            self.assertIn(row, slots.TITLES)
            self.assertTrue(any(row in rows_ for _n, rows_, _d in slots.SECTIONS.values()), row)

    def test_catalogue_default_styles_agree_with_the_slot_module(self):
        for row, (_sec, style) in rows.CATALOGUE.items():
            self.assertIn(style, slots.styles_of(row), row)

    # ------------------------------------------------------------ the widget manager
    def start(self, sec='my_movies'):
        slots.apply_defaults()
        self.commands.clear()
        slots.open_manager(sec)
        return slots._draft()

    def press(self, op, row=0):
        self.info['Container(9000).CurrentItem'] = str(row + 1)                       # the focused list row
        slots.wm_op(op)

    def types(self):
        return [w['type'] for w in slots._draft()['widgets']]

    def test_manager_opens_with_the_active_widgets_and_labels(self):
        draft = self.start()
        self.assertIn('ActivateWindow(1123)', self.commands)
        self.assertEqual(self.types(), ['my_movies_progress', 'my_movies_added', 'my_movies_genres'])
        self.assertEqual(self.strings['mil.edit.1'], 'T32300')
        self.assertEqual(self.strings['mil.edit.1.info'], '20 · T32330 · T32341 · T32377')   # count · sort · style · click
        self.assertEqual(self.strings['mil.edit.3.info'], '20')                          # genres: nothing to choose
        self.assertNotIn('mil.edit.4', self.strings)                                      # empty slots hide their row
        self.assertEqual(self.strings['mil.edit.add'], 'T32364')
        self.assertEqual(self.strings['mil.edit.header'], 'T32351')
        self.assertEqual(self.strings['mil.edit.count'], '3 / {}'.format(slots.SLOTS))    # shown next to "Aktivní widgety"
        self.assertNotIn('mil.edit.dirty', self.strings)

    def test_move_up_and_down_swap_and_follow_with_the_cursor(self):
        self.start()
        self.press('down', 0)
        self.assertEqual(self.types(), ['my_movies_added', 'my_movies_progress', 'my_movies_genres'])
        self.assertIn('Control.Move(9000,1)', self.commands)
        self.assertEqual(self.strings['mil.edit.dirty'], 'T32374')                        # marked as changed
        self.press('up', 2)
        self.assertEqual(self.types(), ['my_movies_added', 'my_movies_genres', 'my_movies_progress'])
        self.assertIn('Control.Move(9000,-1)', self.commands)
        self.commands.clear()
        self.press('up', 0)                                                               # already first: nothing
        self.press('down', 2)                                                             # already last
        self.assertEqual(self.commands, [])

    def test_context_identity_and_configuration_follow_moved_widget(self):
        from wcs.skin import context
        draft = self.start()
        identity = draft['widgets'][0]['uid']
        config = context.normalize(draft['widgets'][0]['type'], {'main': ['detail'], 'hidden': ['play']})
        draft['widgets'][0]['menu'] = config
        slots._store(draft)
        self.press('down', 0)
        self.press('save', 0)
        moved = slots.current('my_movies', 2)
        self.assertEqual(moved['uid'], identity)
        self.assertEqual(moved['menu'], config)
        self.assertIsNone(slots.current('my_movies', 1)['menu'])

    def test_remove_and_add_keep_the_list_packed_and_use_each_row_once(self):
        self.start()
        self.press('remove', 1)
        self.assertEqual(self.types(), ['my_movies_progress', 'my_movies_genres'])
        self.assertEqual(self.strings['mil.edit.count'], '2 / {}'.format(slots.SLOTS))
        self.selected = [0]                                                               # first row not yet used
        self.press('add', 2)                                                              # the "+ Přidat" row is the 3rd
        self.assertEqual(self.types(), ['my_movies_progress', 'my_movies_genres', 'my_movies_added'])
        self.assertEqual(slots._draft()['widgets'][2]['style'], 'poster')                 # starts from its own defaults
        for _ in range(slots.SLOTS - len(self.types())):                                  # fill every available slot
            self.selected = [0]
            self.press('add', len(self.types()))
        self.assertEqual(len(self.types()), slots.SLOTS)
        self.assertEqual(self.strings.get('mil.edit.add', ''), '')                        # no more room: the row disappears
        self.selected = [0]
        self.press('add', slots.SLOTS)
        self.assertEqual(len(self.types()), slots.SLOTS)

    def test_edit_one_value_at_a_time_in_one_window(self):
        self.start()
        # entries of widget 1: content, count, sort, style, done; each value click opens its own chooser
        self.selected = [1, 3, 2, 1, 3, 0, 4]                                             # count 50, sort 'added', style poster
        self.press('edit', 0)
        widget = slots._draft()['widgets'][0]
        self.assertEqual((widget['limit'], widget['sort'], widget['style']), ('50', 'added', 'poster'))
        self.assertEqual(widget['type'], 'my_movies_progress')

    def test_changing_the_content_resets_the_values_and_cancel_keeps_them(self):
        self.start()
        self.selected = [0, 2, 4]                                                         # content -> 3rd unused row, then done
        self.press('edit', 0)
        widget = slots._draft()['widgets'][0]
        self.assertEqual((widget['type'], widget['limit'], widget['sort'], widget['style']),
                         ('my_movies_watched', '20', 'activity', 'poster'))               # its own defaults, not the old ones
        self.start()
        self.selected = [-1]                                                              # first dialog cancelled
        self.press('edit', 0)
        self.assertEqual(self.types()[0], 'my_movies_progress')

    def test_continue_may_show_the_mytv_widgets_with_their_own_style(self):
        self.start('cont')
        self.selected = [0]                                                               # the 1st unused row of Pokračovat
        self.press('add', 3)
        self.assertEqual(self.types()[3], 'widget_mytv_next')
        self.assertEqual(slots._draft()['widgets'][3]['style'], 'episode')                # as in MyTV, not a free choice
        self.selected = [0]
        self.press('add', 4)
        self.assertEqual(self.types()[4], 'widget_mytv_channels')
        self.assertEqual((self.strings['mil.edit.4.info'], self.strings['mil.edit.5.info']), ('T32377', 'T32391'))   # only the click
        self.press('save', 0)
        self.assertEqual(self.strings['mil.cont.4.type'], 'widget_mytv_next')
        self.assertEqual(self.strings['mil.cont.4.style'], 'episode')
        self.assertEqual(self.strings['mil.cont.5.style'], 'poster')
        self.assertEqual(self.strings['mil.cont.4.title'], 'T32316')
        self.assertEqual((self.strings['mil.cont.4.click'], self.strings['mil.cont.5.click']), ('play', 'live'))       # their own defaults

    def test_channel_rows_offer_content_click_and_context_manager(self):
        slots.apply_defaults()
        slots.open_manager('mytv')
        self.assertEqual(self.strings.get('mil.edit.1.info', ''), 'T32391')            # only the click (the channel starts its broadcast)
        shown = []
        original = xbmcgui.Dialog.select
        xbmcgui.Dialog.select = lambda dialog, heading, labels, *a, **k: shown.append(list(labels)) or -1
        try:
            self.press('edit', 0)
        finally:
            xbmcgui.Dialog.select = original
        self.assertEqual(shown[0], ['T32365', 'T32375', 'Context menu', 'T32369'])

    def test_save_packs_the_widgets_writes_the_strings_and_reloads_once(self):
        self.start()
        self.press('remove', 0)                                                           # drop "in progress"
        self.press('hw', 0)
        self.commands.clear()
        self.press('save', 0)
        self.assertEqual([self.strings['mil.my_movies.{}.type'.format(n)] for n in (1, 2, 3)], ['my_movies_added', 'my_movies_genres', 'off'])
        self.assertEqual(self.strings['mil.my_movies.hidewatched'], '1')
        self.assertEqual(self.commands.count('ReloadSkin()'), 1)
        self.assertIn('Dialog.Close(1123)', self.commands)
        self.assertIsNone(slots._draft())                                                 # the draft is gone

    def test_cancel_discards_and_asks_only_when_something_changed(self):
        self.start()
        self.press('cancel', 0)                                                           # unchanged: closes without asking
        self.assertIn('Dialog.Close(1123)', self.commands)
        self.start()
        self.press('down', 0)
        asked = []
        original = xbmcgui.Dialog.yesno
        xbmcgui.Dialog.yesno = lambda dialog, heading, message, *a, **k: asked.append(message) or False
        self.commands.clear()
        try:
            self.press('cancel', 0)
        finally:
            xbmcgui.Dialog.yesno = original
        self.assertEqual(asked, ['T32370'])
        self.assertNotIn('Dialog.Close(1123)', self.commands)                              # "No": the manager stays
        self.assertNotIn('ReloadSkin()', self.commands)
        self.assertEqual(self.strings.get('mil.my_movies.1.type'), 'my_movies_progress')  # the real strings were never touched

    def test_defaults_button_restores_the_section_defaults_in_the_draft_only(self):
        self.start('my_series')
        self.press('remove', 0)
        self.press('defaults', 0)
        self.assertEqual(self.types(), ['my_series_next', 'my_series_new', 'my_series_genres'])
        self.assertEqual(self.strings['mil.edit.up'], 'T32349: T32346')                   # "show unaired": on by default
        self.assertEqual(self.strings['mil.my_series.1.type'], 'my_series_next')           # real strings untouched until Save

    def test_info_key_on_a_menu_item_opens_the_manager_of_its_section(self):
        slots.apply_defaults()
        self.commands.clear()
        slots.run({'action': 'skin_menu_item', 'menu': 'mil_myseries'})
        slots.run({'action': 'skin_menu_item', 'menu': 'video'})                         # not ours: nothing
        self.assertEqual(self.commands.count('ActivateWindow(1123)'), 1)
        self.assertEqual(slots._draft()['sec'], 'my_series')

    def test_genre_rows_force_the_categories_style_and_old_rows_stay_fixed(self):
        self.assertEqual(slots.styles_of('my_movies_genres'), ('categories',))
        self.assertEqual(slots.styles_of('cont_series'), ('episode',))
        self.assertEqual(slots.styles_of('widget_mytv_next'), ('episode',))
        self.assertEqual(slots.styles_of('my_movies_all'), ('poster', 'landscape'))

    def test_defaults_per_section_reset_reloads_the_skin(self):
        slots.apply_defaults()
        self.strings['mil.my_movies.1.type'] = 'my_movies_all'
        slots.run({'action': 'skin_slots_defaults', 'sec': 'my_movies'})
        self.assertEqual(self.strings['mil.my_movies.1.type'], 'my_movies_progress')
        self.assertIn('ReloadSkin()', self.commands)

    def test_generated_include_matches_the_section_table(self):
        sys.path.insert(0, os.path.join(HERE, '..', 'skin_harness'))
        import gen_slots
        self.assertEqual(set(gen_slots.SECTIONS), set(slots.SECTIONS))
        self.assertEqual(gen_slots.SLOTS, slots.SLOTS)
        with open(gen_slots.OUT, encoding='utf-8') as handle:
            self.assertEqual(handle.read(), gen_slots.build())                          # the committed file is current


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


class RealDefaultsTests(unittest.TestCase):
    def test_the_shipped_defaults_are_the_users_layout(self):
        s = slots.SECTIONS
        self.assertEqual([d[0] for d in s['cont'][2]], ['widget_mytv_channels', 'cont_series', 'cont_movies'])
        self.assertEqual([d[0] for d in s['my_movies'][2]], ['my_movies_genres', 'my_movies_progress', 'my_movies_added'])
        self.assertEqual([(d[0], d[1]) for d in s['my_series'][2]][2], ('my_series_added', '10'))
