# -*- coding: utf-8 -*-
"""Targeted menu tests: identity, inheritance, dynamic eligibility and remote edit boundaries."""
import os
import sys
import tempfile
import unittest
import types
from unittest.mock import patch
from urllib.parse import parse_qsl, quote, urlencode, urlsplit

sys.path.insert(0, os.path.dirname(__file__))
import stubs
stubs.install()
import xbmc
import xbmcaddon
import xbmcgui
import xbmcplugin
from wcs.skin import context, slots
from wcs.library2 import meta, rows, store
from wcs.vinyl import client
from wcs.vinyl import context as vinyl


class ContextTests(unittest.TestCase):
    def setUp(self):
        store.set_path(os.path.join(tempfile.mkdtemp(), 'library.db'))
        xbmcaddon.Addon.strings.update({i: 'S{}'.format(i) for i in range(32300, 32634)})
        xbmcaddon.Addon.strings[32400] = 'Resume {}'
        xbmcaddon.Addon.strings[32404] = 'Next {}'
        xbmcaddon.Addon.settings.clear()

    def test_channel_edit_opens_the_editor_and_preserves_widget_origin(self):
        from wcs.skin import channel_actions
        channel = types.SimpleNamespace(id='check', name='Check')
        manager = types.SimpleNamespace(get_channel_by_id=lambda identity: channel)
        module = types.SimpleNamespace(MediaChannelManager=lambda *a, **kw: manager)
        with patch.dict(sys.modules, {'wcs.ai.MediaChannelManager': module}), patch.object(xbmc, 'executebuiltin') as execute:
            channel_actions.run({'kind': 'cinema', 'id': 'check', 'op': 'edit_channel', 'ctx': 'widget-2', 'ctxrow': 'widget_mycinema_channels'})
        query = dict(parse_qsl(urlsplit(execute.call_args[0][0][len('RunPlugin('):-1]).query))
        self.assertEqual(query['action'], 'my_cinema_dialog')
        self.assertEqual(query['edit'], '1')
        self.assertEqual(query['ctx'], 'widget-2')

    def test_channel_create_cards_keep_their_original_behavior(self):
        item = xbmcgui.ListItem('Add series')
        item.setProperty('channel_id', 'check')
        item.setProperty('is_add_button', 'true')
        control = types.SimpleNamespace(getSelectedPosition=lambda: 0, getListItem=lambda i: item)
        dialog = types.SimpleNamespace(getFocusId=lambda: 9100, getControl=lambda i: control)
        self.assertFalse(context.channel_dialog_context(dialog))

    def test_every_widget_has_complete_unique_defaults(self):
        for row in slots.TITLES:
            config = context.defaults(row)
            actions = config['main'] + config['more'] + config['hidden']
            self.assertEqual(set(actions), set(context.catalog(row)), row)
            self.assertEqual(len(actions), len(set(actions)), row)
            self.assertTrue(config['main'], row)
            self.assertTrue(all(a in context.LABELS for a in actions), row)

    def test_custom_order_hidden_and_new_actions_survive_normalization(self):
        config = context.normalize('my_movies_all', {'main': ['detail', 'play', 'detail'], 'more': ['source'], 'hidden': ['list']})
        self.assertEqual(config['main'], ['detail', 'play'])
        self.assertEqual(config['hidden'], ['list'])
        self.assertNotIn('list', config['more'])
        self.assertIn('watched', config['more'])
        self.assertEqual(context.decode(context.encode(config)), config)
        self.assertNotIn('"', context.encode(config))

    def test_draft_snapshot_is_independent_of_nested_menu_changes(self):
        widget = slots._widget('my_movies_all', 'my_movies')
        widget['menu'] = context.defaults(widget['type'])
        draft = {'widgets': [widget], 'hw': False, 'up': False}
        original = slots._snapshot(draft)
        widget['menu']['main'].reverse()
        self.assertNotEqual(original, slots._snapshot(draft))

    def test_both_transport_levels_preserve_unicode_urls_and_ampersands(self):
        data = {'type': 'vinyl', 'name': 'A & B, 100%20', 'ctx': 'widget-1',
                'path': context.BASE + '?action=vinyl_item&id=12&name=' + quote('A & B')}
        packed = quote(urlencode(data), safe='')
        self.assertEqual(context.unpack(packed), data)
        parsed = dict(parse_qsl('action=ctx_open&data=' + packed))
        self.assertEqual(context.unpack(parsed['data']), data)

    def test_folder_and_nested_skin_run_keep_origin_across_navigation(self):
        path = context.BASE + '?action=mlist&kind=seasons&tmdb=42'
        item = xbmcgui.ListItem('Seriál', path=path)
        item.setProperty('milionar.type', 'tv')
        item.setProperty('milionar.tmdb', '42')
        path, item, folder = context.attach(path, item, True, 'widget-a', 'my_series_all')
        self.assertEqual(dict(parse_qsl(urlsplit(path).query))['ctx'], 'widget-a')
        self.assertEqual(context.unpack(item.getProperty('milionar.ctxdata'))['type'], 'tv')
        nested = context.with_origin(context.BASE + '?action=skin_run&run=' + quote('action=my_tv_dialog&channel_id=c'), 'widget-a', 'widget_mytv_channels')
        params = dict(parse_qsl(urlsplit(nested).query))
        self.assertEqual(dict(parse_qsl(params['run']))['ctx'], 'widget-a')

    def test_output_decorator_covers_legacy_single_and_multiple_items(self):
        one, many = xbmcplugin.addDirectoryItem, xbmcplugin.addDirectoryItems
        try:
            context.install_listing_context({'ctx': 'origin', 'ctxrow': 'widget_csfd_charts'})
            path = context.BASE + '?action=mlist&kind=movies'
            xbmcplugin.addDirectoryItem(81, path, xbmcgui.ListItem('a'), True)
            xbmcplugin.addDirectoryItems(81, [(path, xbmcgui.ListItem('b'), True)])
            for url, item, folder in xbmcplugin.ITEMS[81]:
                self.assertEqual(dict(parse_qsl(urlsplit(url).query))['ctx'], 'origin')
                self.assertEqual(context.unpack(item.getProperty('milionar.ctxdata'))['ctx'], 'origin')
        finally:
            xbmcplugin.addDirectoryItem, xbmcplugin.addDirectoryItems = one, many

    def test_dynamic_movie_labels_and_collection_filter(self):
        meta.put(meta.key_movie(31), {'id': 31, 'title': 'Film'}, 10**9)
        data = {'type': 'movie', 'tmdb': '31', 'title': 'Film', 'row': 'my_movies_all'}
        first = context.video_actions(data)
        self.assertNotIn('collection', first)
        self.assertEqual(first['play'][0], 'S32402')
        store.save_resume('movie', 31, position=90, total=1000)
        second = context.video_actions(data)
        self.assertTrue(second['play'][0].startswith('Resume '))
        self.assertIn('from=resume', second['play'][1])
        self.assertIn('from=start', second['restart'][1])

    def test_user_hidden_action_is_absent_but_manager_links_are_fixed(self):
        widget = {'type': 'my_movies_all', 'menu': context.normalize('my_movies_all', {'main': ['detail'], 'hidden': ['play']})}
        with patch.object(context, 'find_widget', return_value=('my_movies', 2, widget)), patch.object(context, 'video_actions', return_value={'play': ('Play', 'p'), 'detail': ('Detail', 'd')}):
            top, more = context.parts({'ctx': 'a', 'type': 'movie', 'tmdb': '1'})
        self.assertEqual(top, [('Detail', 'd')])
        self.assertEqual(len(more), 3)                      # a hero section also links its hero button editor
        self.assertIn('action=skin_slots', more[0][1])
        self.assertIn('action=skin_context_edit', more[1][1])
        self.assertIn('action=skin_hero', more[2][1])

    def test_no_owned_playlist_edits_for_public_playlist(self):
        with patch.object(client, 'account', return_value='7'), patch.object(client, 'cached', return_value=[]):
            actions = vinyl.actions({'kind': 'playlist', 'id': '8', 'name': 'Public'})
        self.assertIn('favorite', actions)
        self.assertNotIn('delete_playlist', actions)
        self.assertNotIn('rename_playlist', actions)

    def test_unauthenticated_menu_has_only_read_actions(self):
        with patch.object(client, 'account', return_value=''), patch.object(client, 'cached', return_value=[]):
            actions = vinyl.actions({'kind': 'song', 'id': '8'})
        self.assertNotIn('favorite', actions)
        self.assertNotIn('add_playlist', actions)
        self.assertNotIn('create_playlist', actions)

    def test_playlist_ownership_is_validated_before_any_write(self):
        with patch.object(client, 'account', return_value='7'), patch.object(client, 'request', return_value={'user_id': 99, 'name': 'Other'}) as request:
            with self.assertRaises(PermissionError):
                vinyl.run({'op': 'delete_playlist', 'kind': 'playlist', 'id': '8'})
        request.assert_called_once_with('playlists/8')

    def test_playlist_delete_never_deletes_library_songs(self):
        with patch.object(client, 'account', return_value='7'), patch.object(client, 'request', return_value={'user_id': 7, 'name': 'Own'}) as request, patch.object(xbmcgui.Dialog, 'yesno', return_value=True), patch('wcs.vinyl.widgets.refresh', return_value=True):
            vinyl.run({'op': 'delete_playlist', 'kind': 'playlist', 'id': '8'})
        self.assertEqual(request.call_args_list[-1].args, ('playlists/8', 'DELETE'))
        self.assertFalse(any('deleteSongs' in str(call) for call in request.call_args_list))

    def test_album_favorite_retains_identity_and_uses_proxy_client(self):
        with patch.object(client, 'account', return_value='7'), patch.object(client, 'request') as request, patch('wcs.vinyl.widgets.refresh', return_value=True):
            vinyl.run({'op': 'favorite', 'kind': 'album', 'id': '8', 'name': 'A & B', 'artist': 'Umělec'})
        path, method = request.call_args.args
        self.assertEqual(method, 'PATCH')
        self.assertTrue(path.startswith('browse/albums/A%20%26%20B/favorite?'))
        self.assertEqual(dict(parse_qsl(path.split('?', 1)[1]))['albumId'], '8')

    def test_more_back_returns_to_parent_without_running_an_action(self):
        with patch.object(context, 'parts', return_value=([('Play', 'p')], [('Detail', 'd')])), \
                patch.object(xbmcgui.Dialog, 'contextmenu', side_effect=[1, -1, -1]) as native, \
                patch.object(xbmc, 'executebuiltin') as execute:
            context.open_menu({})
        self.assertEqual([call.args[0] for call in native.call_args_list], [['Play', 'S32420'], ['Detail'], ['Play', 'S32420']])
        execute.assert_not_called()
        self.assertFalse(xbmcgui.Window(10000).getProperty('milionar.ctx.active'))

    def test_native_menu_error_clears_recursion_guard(self):
        with patch.object(context, 'parts', return_value=([('Play', 'p')], [])), \
                patch.object(xbmcgui.Dialog, 'contextmenu', side_effect=RuntimeError('check')):
            with self.assertRaises(RuntimeError):
                context.open_menu({})
        self.assertFalse(xbmcgui.Window(10000).getProperty('milionar.ctx.active'))

    def test_manager_moves_hides_and_saves_without_mutating_parent_draft(self):
        from wcs.skin.context_dialogs import MenuManager
        config = context.defaults('my_movies_all')
        original = context.decode(context.encode(config))
        dialog = MenuManager('', '', row='my_movies_all', config=config)
        dialog.onInit()
        dialog.getControl(9000).selectItem(0)
        dialog.onClick(9102)
        self.assertEqual(dialog.config['main'][:2], ['detail', 'play'])
        dialog.onClick(9105)
        self.assertIn('play', dialog.config['hidden'])
        dialog.onClick(9108)
        self.assertTrue(dialog.closed)
        self.assertIn('play', dialog.result['hidden'])
        self.assertEqual(config, original)

    def test_playlist_reorder_at_edges_hides_impossible_directions(self):
        with patch.object(client, 'account', return_value='7'), patch.object(client, 'cached', return_value=[]):
            actions = vinyl.actions({'kind': 'song', 'id': '8', 'parent_playlist': '1', 'owner': '1', 'ids': '8,9'})
        self.assertIn('remove_song', actions)
        self.assertIn('song_down', actions)
        self.assertNotIn('song_up', actions)

    def test_playlist_create_and_rename_use_owner_and_expected_rest_payloads(self):
        with patch.object(client, 'account', return_value='7'), patch.object(xbmcgui.Dialog, 'input', return_value='New', create=True), patch.object(client, 'request', return_value={'user_id': 7, 'name': 'Old'}) as request, patch('wcs.vinyl.widgets.refresh', return_value=True):
            vinyl.run({'op': 'create_playlist'})
            self.assertEqual(request.call_args.args, ('playlists', 'POST', {'name': 'New'}))
            request.reset_mock()
            vinyl.run({'op': 'rename_playlist', 'id': '8'})
            self.assertEqual(request.call_args_list[0].args, ('playlists/8',))
            self.assertEqual(request.call_args.args, ('playlists/8', 'PATCH', {'name': 'New'}))

    def test_remove_song_and_reorder_only_touch_the_owned_playlist(self):
        data = {'user_id': 7, 'name': 'Own', 'songs': [{'id': 8}, {'id': 9}]}
        with patch.object(client, 'account', return_value='7'), patch.object(xbmcgui.Dialog, 'yesno', return_value=True), patch.object(client, 'request', return_value=data) as request, patch('wcs.vinyl.widgets.refresh', return_value=True):
            vinyl.run({'op': 'remove_song', 'id': '8', 'parent_playlist': '1'})
            self.assertEqual(request.call_args.args, ('playlists/1/songs/8', 'DELETE'))
            request.reset_mock()
            vinyl.run({'op': 'song_down', 'id': '8', 'parent_playlist': '1'})
            self.assertEqual(request.call_args.args, ('playlists/1/reorder', 'PATCH', {'song_ids': [9, 8]}))

    def test_child_song_of_playlist_gets_playback_in_default_main_menu(self):
        data = {'type': 'vinyl', 'kind': 'song', 'id': '8', 'row': 'vinyl_curator', 'ids': '8,9'}
        with patch.object(client, 'account', return_value='7'), patch.object(client, 'cached', return_value=[]):
            top, more = context.parts(data)
        self.assertTrue(any('click=play&' in c or c.endswith('click=play)') for _, c in top))
        self.assertTrue(any('click=queue' in c for _, c in top))

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