"""Offline Home navigation: preserve widget configuration and reuse native listing actions."""
import copy
import json
from pathlib import Path
import sys
import unittest
from unittest.mock import Mock, patch
import xml.etree.ElementTree as ET

ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(ROOT / 'scripts' / 'library_harness'))
import stubs
stubs.install()

import xbmc
import xbmcaddon
import xbmcgui
from wcs.library2 import listings, urls
from wcs.skin import context, context_dialogs, hero_buttons, navigation, slots


class NavigationTests(unittest.TestCase):
    def setUp(self):
        strings = dict(xbmcaddon.Addon.strings)
        strings.update({31118: 'Add movie', 31035: 'Add series', 32754: 'Classic list'})
        localized = patch.object(xbmcaddon.Addon, 'strings', strings)
        localized.start()
        self.addCleanup(localized.stop)

    def test_every_sidebar_click_has_the_right_arrow_target(self):
        root = ET.parse(ROOT / 'resources/skin.milionar/xml/Home.xml').getroot()
        menu = root.find('.//control[@id="9000"]')
        self.assertEqual(menu.findtext('onright'), 'SetFocus($INFO[Container(9000).ListItem.Property(menu_id)])')
        items = menu.findall('content/item')
        self.assertEqual(len(items), 11)
        for item in items:
            target = item.find('property[@name="menu_id"]').text.removeprefix('$NUMBER[').removesuffix(']')
            self.assertEqual(item.findtext('onclick'), 'SetFocus({})'.format(target))

    def test_home_and_native_lists_share_the_add_path(self):
        for sec, kind in (('my_movies', 'movies'), ('my_series', 'series')):
            path = urls.add_url(kind)
            self.assertEqual(navigation.command(sec, 'add'), 'RunPlugin({})'.format(path))
            self.assertIn(path, [url for url, _item, _folder in listings._top_buttons(kind, False)])
            self.assertIn('new=1', path)

    def test_classic_destinations_keep_the_existing_lists(self):
        self.assertIn('mlist&kind=movies&skin=1', navigation.command('my_movies'))
        self.assertIn('mlist&kind=series&skin=1', navigation.command('my_series'))
        self.assertEqual(navigation.command('vinyl'), 'ActivateWindow(Music,plugin://plugin.video.milionar/?action=vinyl_browse,return)')

    def test_action_cards_have_no_media_hero_and_keep_widget_origin(self):
        for row, sec in navigation.ROWS.items():
            with patch.object(context, 'origin', return_value='custom-uid'):
                result = navigation.items(row, {'sec': sec, 'slot': '7'})
            self.assertEqual(len(result), 1 if sec == 'vinyl' else 2)
            for _url, item, folder in result:
                data = context.unpack(item.getProperty('milionar.ctxdata'))
                self.assertEqual(data['ctx'], 'custom-uid')
                self.assertEqual(data['row'], row)
                self.assertEqual(data['type'], 'action')
                self.assertFalse(folder)
                self.assertFalse(item.getProperty('milionar.hero.title'))
                self.assertTrue(item.getProperty('milionar.exec').startswith(('ActivateWindow(', 'RunPlugin(')))

    def test_migration_appends_after_six_without_touching_existing_widgets(self):
        originals = [dict(type='vinyl_discover', uid='u{}'.format(n), menu={'hidden': ['favorite']},
                          hero={'main': ['play_all']}) for n in range(6)] + [dict(type='off')]
        before = copy.deepcopy(originals)
        with patch.object(slots, 'current', side_effect=lambda sec, n: originals[n - 1]), \
             patch.object(slots, 'write_slot') as write, patch.object(slots, 'reload_skin'), \
             patch.object(xbmc, 'executebuiltin'):
            navigation.migrate()
        self.assertEqual(originals, before)
        self.assertEqual(write.call_count, 3)
        for call in write.call_args_list:
            self.assertEqual(call.args[1], 7)
            self.assertEqual(call.args[2], navigation.ROW_FOR[call.args[0]])

    def test_migration_does_not_duplicate_or_recreate_a_moved_row(self):
        with patch.object(slots, 'current', side_effect=lambda sec, n: {'type': navigation.ROW_FOR[sec] if n == 2 else 'off'}), \
             patch.object(slots, 'write_slot') as write, patch.object(slots, 'reload_skin'), \
             patch.object(xbmc, 'executebuiltin'):
            navigation.migrate()
        write.assert_not_called()

    def test_sidebar_context_opens_the_manager_directly_for_every_section(self):
        for menu_id in slots.MENU_SECTIONS:
            with patch.object(xbmc, 'getSkinDir', return_value='skin.milionar'), \
                 patch.object(context, 'focused_control', return_value='9000'), \
                 patch.object(xbmc, 'getInfoLabel', return_value=menu_id), \
                 patch.object(xbmcgui.Dialog, 'contextmenu') as popup, \
                 patch.object(slots, 'open_for_menu_item') as manager:
                context.home_context()
            manager.assert_called_once_with(menu_id)
            popup.assert_not_called()

    def test_defaults_append_the_row_and_manager_exposes_slot_seven(self):
        shipped = json.loads((ROOT / 'resources/defaults/skin.json').read_text())
        for sec, row in navigation.ROW_FOR.items():
            self.assertEqual(shipped['sections'][sec]['widgets'][-1]['type'], row)
            self.assertEqual(slots.SECTIONS[sec][2][-1][0], row)
            self.assertIn(row, slots.SECTIONS[sec][1])
            self.assertEqual(slots.styles_of(row), ('categories',))
            self.assertEqual(context.catalog(row), navigation.catalog(row))
        manager = (ROOT / 'resources/skin.milionar/xml/Custom_1123_MilionarRows.xml').read_text()
        self.assertIn('Skin.String(mil.edit.7)', manager)

    def test_classic_listing_has_no_fixed_buttons_or_navigation_targets(self):
        root = ET.parse(ROOT / 'resources/skin.milionar/xml/Includes_MilionarCinema.xml').getroot()
        for sec, ident in (('my_movies', 29950), ('my_series', 30950), ('vinyl', 31950)):
            section = root.find('include[@name="MilionarCinema_' + sec + '"]')
            button = section.find('.//control[@id="{}"]'.format(ident))
            self.assertIsNone(button)
            for direction in ('onup', 'ondown', 'onleft', 'onright', 'onback'):
                self.assertNotIn(str(ident), [node.text for node in section.iter(direction)])
        self.assertFalse([c for c in root.iter('control') if (c.findtext('onclick') or '').startswith('RunPlugin(plugin://plugin.video.milionar/?action=skin_classic')])

    def test_add_card_is_first_and_both_actions_are_in_the_editors(self):
        for row in ('home_movies_actions', 'home_series_actions'):
            expected = ['add', 'browse']
            self.assertEqual(context.defaults(row)['main'], expected)
            self.assertEqual(hero_buttons.defaults(row)['main'], expected)
            cards = navigation.items(row, {})
            self.assertIn('tmdb_add_', cards[0][1].getProperty('milionar.exec'))
            self.assertIn('action=mlist', cards[1][1].getProperty('milionar.exec'))
            for model in (context_dialogs.Menu, context_dialogs.Hero):
                editor = context_dialogs.MenuManager.__new__(context_dialogs.MenuManager)
                editor.row, editor.model = row, model
                editor.config = model.normalize(row, None)
                control = Mock()
                editor.getControl = lambda _ident: control
                editor._fill()
                self.assertEqual(editor.entries, [('main', 'add'), ('main', 'browse')])
                self.assertEqual([item.getLabel() for item in control.addItems.call_args.args[0]],
                                 [navigation.label(row, action) for action in expected])

    def test_legacy_open_setting_upgrades_and_custom_visibility_stays_independent(self):
        row = 'home_movies_actions'
        old = {'main': ['open'], 'more': [], 'hidden': []}
        self.assertEqual(context.normalize(row, old)['main'], ['add', 'browse'])
        self.assertEqual(old['main'], ['open'])
        custom = {'main': ['browse'], 'more': [], 'hidden': ['add']}
        self.assertEqual(context.normalize(row, custom), custom)
        self.assertEqual(hero_buttons.normalize(row, custom), custom)

    def test_each_card_context_exposes_both_real_actions(self):
        row = 'home_series_actions'
        for _url, item, _folder in navigation.items(row, {}):
            data = context.unpack(item.getProperty('milionar.ctxdata'))
            self.assertEqual(list(context.resolve(data)[-1]), ['add', 'browse'])
            with patch.object(context, 'find_widget', return_value=('my_series', 7, dict(type=row, menu=None, hero=None))):
                top, _more = context.parts(dict(data, ctx='test-widget'))
            self.assertEqual(tuple(top[0]), (navigation.label(row, 'add'), navigation.command('my_series', 'add')))
            self.assertEqual(top[1][0], navigation.label(row, 'browse'))
            self.assertIn('action=mlist&kind=series&skin=1', top[1][1])

    def test_borrowed_hero_uses_row_actions_and_independent_order(self):
        row = 'home_movies_actions'
        item = xbmcgui.ListItem('A title')
        item.setProperty('milionar.hero.title', 'A title')
        config = {'main': ['browse', 'add'], 'more': [], 'hidden': []}
        hero_buttons.compose(item, {'type': 'movie', 'tmdb': '603'}, row, config, 'encoded', prefix='s7.')
        self.assertEqual(item.getProperty('milionar.hero.s7.b1'), navigation.command('my_movies'))
        self.assertEqual(item.getProperty('milionar.hero.s7.b2'), navigation.command('my_movies', 'add'))

    def test_borrowed_hero_more_runs_the_action_of_its_widget(self):
        row = 'home_movies_actions'
        widget = {'type': row, 'hero': {'main': ['browse'], 'more': ['add'], 'hidden': []}}
        with patch.object(context, 'resolve', return_value=('', 0, None, 'my_movies_added', {})), \
             patch.object(context, 'find_widget', return_value=('my_movies', 7, widget)), \
             patch.object(xbmcgui.Dialog, 'contextmenu', return_value=0), \
             patch.object(xbmc, 'executebuiltin') as execute:
            hero_buttons.run({'do': 'more', 'owner': 'actions-widget'})
        execute.assert_called_once_with(navigation.command('my_movies', 'add'))

    def test_missing_headers_are_repaired_at_the_current_widget_positions(self):
        strings = {'mil.my_movies.4.type': 'home_movies_actions', 'mil.my_movies.4.title': '',
                   'mil.my_series.2.type': 'home_series_actions', 'mil.my_series.2.title': '',
                   'mil.vinyl.7.type': 'home_vinyl_actions', 'mil.vinyl.7.title': 'Custom title'}
        with patch.object(slots, '_text', return_value='Browse'), \
             patch.object(xbmc, 'getInfoLabel', side_effect=lambda name: strings.get(name[len('Skin.String('):-1], '')), \
             patch.object(slots, '_set') as write, patch.object(xbmc, 'executebuiltin'):
            self.assertTrue(navigation.repair_titles())
        self.assertEqual([call.args for call in write.call_args_list],
                         [('mil.my_movies.4.title', 'Browse'), ('mil.my_series.2.title', 'Browse')])

    def test_header_repair_waits_until_translations_are_available(self):
        with patch.object(slots, '_text', return_value=''), patch.object(slots, '_set') as write, \
             patch.object(xbmc, 'executebuiltin') as execute:
            self.assertFalse(navigation.repair_titles())
        write.assert_not_called()
        execute.assert_not_called()


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