"""Focused offline checks of Vinyl artwork, Home clicks and explicit detail paging."""
from pathlib import Path
import sys
import unittest
from unittest.mock import patch
from urllib.parse import parse_qs, urlsplit
import xml.etree.ElementTree as ET

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 gen_vinyl_detail
import xbmc
import xbmcaddon
import xbmcgui
from wcs.skin import context
from wcs.vinyl import client, widgets, presentation, detail, defaults


class VinylCinemaTests(unittest.TestCase):
    def setUp(self):
        self.addCleanup(patch.stopall)
        patch.object(xbmcaddon.Addon, 'settings', {'vinyl_user_id': '11', 'vinyl_token': 'synthetic'}).start()
        patch.object(xbmcaddon.Addon, 'strings', {32530: '{} tracks', 32689: 'Playlist', 32503: 'Tracks'}).start()
        patch.object(client, 'cached', return_value=[]).start()
        self.entry = dict(id=7, name='A & B, 100%', cover_mode='custom', cover_version=3,
                          title_image_version=2, song_count=15, total_duration=4521,
                          description='Playlist description', is_favorite=False)
        self.params = widgets.query(self.entry, 'playlist', 'vinyl_curator')
        self.songs = [dict(id=i, title='Track {}'.format(i), artist='Artist', duration=180) for i in (9, 3, 5)]

    def test_card_builder_and_origin_decorator_preserve_detail_click_and_hero(self):
        with patch.object(client, 'request', side_effect=AssertionError('Widgets cannot fetch')):
            path, item, folder = widgets.item(self.entry, 'playlist', 'vinyl_curator')
            path, item, folder = context.attach(path, item, folder, 'widget-1', 'vinyl_curator')
        self.assertFalse(folder)
        self.assertEqual(parse_qs(urlsplit(path).query)['action'], ['vinyl_detail'])
        self.assertIn('action=vinyl_detail', item.getProperty('milionar.exec'))
        self.assertEqual(item.getProperty('milionar.hero.directs'), 'play_all,open,shuffle,favorite')
        self.assertIn('ctx=widget-1', item.getProperty('milionar.hero.do.favorite'))
        self.assertIn('ctx=widget-1', item.getProperty('milionar.hero.do.shuffle'))
        self.assertIn('ctx=widget-1', item.getProperty('milionar.hero.detail'))
        self.assertIn('click=play_all', item.getProperty('milionar.hero.play'))
        self.assertEqual(item.getProperty('milionar.hero.title'), self.entry['name'])
        self.assertIn('title-image?v=2&w=1200', item.getProperty('milionar.hero.title_image'))
        self.assertNotIn('synthetic', str(item.props) + path + str(item.art))

    def test_hero_facts_title_image_and_favorite_button(self):
        strings = {**xbmcaddon.Addon.strings, 32612: 'Add to favorites', 32613: 'Remove from favorites'}
        entry = dict(self.entry, created_at='2026-10-05 20:27:31')
        with patch.object(xbmcaddon.Addon, 'strings', strings), \
                patch.object(presentation.time, 'localtime', presentation.time.gmtime):
            fields = presentation.fields(entry, 'playlist', self.params)
            song = presentation.fields(dict(id=9, title='Track', artist='Artist', duration=185, is_favorite=1), 'song',
                                       widgets.query({'id': 9, 'title': 'Track'}, 'song', 'vinyl_songs', 'play'))
            genre = presentation.fields({'name': 'House', 'song_count': 3}, 'genre', {'kind': 'genre', 'name': 'House'})
        self.assertEqual(fields['meta'], '15 tracks · 1h 15m · 5. 10. 2026 20:27')
        self.assertEqual((fields['size'], fields['left']), ('', ''))              # the title image stands for the title
        self.assertEqual(fields['favorite_label'], 'Add to favorites')
        self.assertIn('action=vinyl_context', fields['favorite'])
        self.assertIn('op=favorite', fields['favorite'])
        self.assertNotIn('click=', fields['favorite'])
        self.assertEqual((song['favorite_label'], song['shuffle'], song['size']), ('Remove from favorites', '', 'l'))
        self.assertEqual(song['meta'], 'Artist · 3:05')
        self.assertEqual((genre['favorite'], genre['favorite_label']), ('', ''))
        strip = lambda value: value.replace('\u200a', '')  # noqa: E731
        self.assertEqual(strip(fields['kicker']), 'PLAYLIST')                  # no genres known: the kind
        tagged = presentation.fields(dict(entry, _genres='Drum and Bass,House,Techno,Trance'), 'playlist', self.params)
        self.assertEqual(strip(tagged['kicker']), 'DRUM AND BASS • HOUSE • TECHNO')
        self.assertEqual(widgets._top_genres([{'genre': 'House'}, {'genre': 'Techno, House'}, {'genre': ''}, {}]), 'House,Techno')
        self.assertEqual(presentation.created('yesterday'), '')
        xbmcaddon.Addon.settings.pop('vinyl_user_id')                          # no account: no favorite button
        self.assertEqual(presentation.fields(entry, 'playlist', self.params)['favorite'], '')

    def test_other_skins_keep_the_existing_collection_folder(self):
        with patch.object(xbmc, 'getSkinDir', return_value='skin.estuary'):
            path, item, folder = widgets.item(self.entry, 'playlist', 'vinyl_curator')
        self.assertTrue(folder)
        self.assertIn('ActivateWindow(Music', item.getProperty('milionar.exec'))
        self.assertEqual(parse_qs(urlsplit(path).query)['action'], ['vinyl_item'])

    def test_hero_original_art_and_supported_low_power_title_variant(self):
        fields = presentation.fields(self.entry, 'playlist', self.params)
        self.assertNotIn('size=', fields['fanart'])
        self.assertIn('size=600', fields['cover'])
        xbmcaddon.Addon.settings['skin_low_power'] = 'true'
        fields = presentation.fields(self.entry, 'playlist', self.params)
        self.assertIn('w=720', fields['title_image'])
        self.assertIn('size=600', fields['fanart'])
        self.assertIn('size=300', fields['cover'])

    def test_missing_title_image_and_genre_art_have_defined_fallbacks(self):
        entry = dict(self.entry, title_image_version=None)
        fields = presentation.fields(entry, 'playlist', self.params)
        self.assertEqual(fields['title_image'], '')
        self.assertEqual(fields['title'], self.entry['name'])
        cover = client.cover({'name': 'House', 'cover_songs': [{'id': 9, 'cover_version': 4}]}, 'genre')
        self.assertIn('cover/9?v=4', cover)
        self.assertEqual(client.cover({'name': 'House'}, 'genre'), 'DefaultMusicGenres.png')

    def test_refresh_keeps_graphic_titles_and_not_secret_playlist_fields(self):
        plain = widgets._plain([dict(self.entry, share_token='private', cover_custom_path='/private')])[0]
        self.assertEqual(plain['title_image_version'], 2)
        self.assertNotIn('share_token', plain)
        self.assertNotIn('cover_custom_path', plain)

    def test_graphic_title_revision_invalidates_the_visible_row(self):
        previous = {'items': [self.entry]}
        new = {'items': [dict(self.entry, title_image_version=3)]}
        self.assertNotEqual(widgets._display(previous, 'curator', [('source', 20)], '11'),
                            widgets._display(new, 'curator', [('source', 20)], '11'))

    def test_popular_tracks_come_from_featured_not_latest_song_library(self):
        with patch.object(client, 'request', return_value={'topTracks': self.songs}) as request:
            result = client.sources(['top_tracks'])
        self.assertEqual(result, {'top_tracks': self.songs})
        request.assert_called_once_with('featured?slim=1&curatorLimit=80')

    def test_playlist_open_fetches_once_and_uses_authoritative_count_and_order(self):
        with patch.object(client, 'request', return_value=dict(self.entry, songs=self.songs)) as request:
            entry, songs, more = detail.load(self.params)
        request.assert_called_once_with('playlists/7')
        self.assertEqual(entry['song_count'], 3)
        self.assertEqual(entry['total_duration'], 540)
        self.assertEqual([song['id'] for song in songs], [9, 3, 5])
        self.assertFalse(more)

    def test_artist_detail_reads_only_the_requested_page(self):
        params = dict(kind='artist', name='A & B')
        with patch.object(client, 'song_page', return_value=(self.songs, True)) as page:
            entry, songs, more = detail.load(params, 20)
        page.assert_called_once_with('browse/artist/A%20%26%20B/songs', 20)
        self.assertTrue(more)

    def dialog(self):
        return detail.VinylDetail(params=self.params, session=client.scope(),
                                  data=(self.entry, self.songs, False))

    def test_detail_track_click_plays_from_selection_in_the_same_order(self):
        dialog = self.dialog()
        dialog.onInit()
        dialog.getControl(500).selectItem(1)
        with patch.object(detail.actions, '_play') as play:
            dialog.onClick(500)
        self.assertEqual([song['id'] for song in play.call_args.args[0]], [3, 5])
        self.assertEqual(play.call_args.args[1], 'play_all')

    def test_account_change_closes_detail_before_any_action(self):
        dialog = self.dialog()
        xbmcaddon.Addon.settings['vinyl_token'] = 'different session'
        with patch.object(detail.actions, '_play') as play, patch.object(client, 'request') as request:
            dialog.onClick(101)
        self.assertTrue(dialog.closed)
        play.assert_not_called()
        request.assert_not_called()

    def test_failed_page_keeps_current_page_and_selection(self):
        dialog = self.dialog()
        dialog.onInit()
        dialog.getControl(500).selectItem(2)
        with patch.object(detail, 'load', side_effect=RuntimeError('HTTP 503')), patch.object(
                xbmcgui, 'NOTIFICATION_ERROR', 'error', create=True):
            dialog.onClick(108)
        self.assertEqual(dialog.offset, 0)
        self.assertEqual(dialog.getControl(500).getSelectedPosition(), 2)
        self.assertEqual(dialog.songs, self.songs)

    def test_generated_detail_is_current_and_assets_exist(self):
        xml = gen_vinyl_detail.build()
        self.assertEqual(gen_vinyl_detail.OUT.read_text(), xml)
        root = ET.fromstring(xml)
        controls = {node.get('id') for node in root.iter('control') if node.get('id')}
        self.assertIn(root.findtext('defaultcontrol'), controls)
        for node in root.iter('texture'):
            if node.text.startswith(gen_vinyl_detail.ART):
                path = gen_vinyl_detail.ROOT / 'resources/skin.milionar/media/milionar/cinema' / node.text.rsplit('/', 1)[1]
                self.assertTrue(path.is_file(), path)
        self.assertEqual(len(controls), len([node for node in root.iter('control') if node.get('id')]))

    def test_cinematic_defaults_move_the_exact_menu_with_each_widget(self):
        import copy
        menu = {'main': ['shuffle', 'open'], 'more': ['queue'], 'hidden': ['favorite', 'play_all']}
        widgets = [dict(type='vinyl_artists', style='square', uid='artists', menu=copy.deepcopy(menu), click='open'),
                   dict(type='vinyl_curator', style='square', uid='curator', menu=copy.deepcopy(menu), click='play_all'),
                   dict(type='vinyl_favorite_songs', style='square', uid='songs', menu=copy.deepcopy(menu), click='play_from')]
        before = copy.deepcopy(widgets)
        updated = defaults.layout(widgets)
        self.assertEqual([item['type'] for item in updated], ['vinyl_curator', 'vinyl_favorite_songs', 'vinyl_artists'])
        self.assertEqual([item['style'] for item in updated], ['banner', 'record', 'artist'])
        self.assertEqual(widgets, before)
        for item in updated:
            previous = next(old for old in widgets if old['uid'] == item['uid'])
            self.assertEqual(item['menu'], previous['menu'])
            self.assertEqual(item['click'], previous['click'])
            self.assertEqual(context.encode(item['menu']), context.encode(previous['menu']))

    def test_defaults_apply_only_once_and_only_to_vinyl(self):
        widget = dict(type='vinyl_curator', limit='50', sort='title', style='square',
                      click='play_all', uid='custom', menu={'main': ['shuffle'], 'more': [], 'hidden': ['open']})
        with patch.object(defaults.slots, 'current', side_effect=lambda _sec, n: widget if n == 3 else {'type': 'off'}), patch.object(
                defaults.slots, '_write_section', return_value=True) as write, patch.object(defaults.slots, 'reload_skin') as reload:
            defaults.apply()
            self.assertEqual(write.call_args.args[0], 'vinyl')
            self.assertEqual(len(write.call_args.args[1]), 6)
            current = next(item for item in write.call_args.args[1] if item.get('uid') == 'custom')
            self.assertEqual(current['menu'], widget['menu'])
            self.assertEqual(current['limit'], '50')
            self.assertEqual(current['sort'], 'title')
            reload.assert_called_once()
            with patch.object(xbmc, 'getCondVisibility', return_value=True):
                defaults.apply()
            self.assertEqual(write.call_count, 1)

    def test_detail_menu_honors_the_existing_context_manager(self):
        from wcs.skin import context as menus
        with patch.object(menus, 'open_menu', return_value=None) as menu:
            detail.options(self.params)
        self.assertEqual(menu.call_args.kwargs, {'execute': False})
        self.assertEqual(menu.call_args.args[0]['type'], 'vinyl')
        self.assertEqual(menu.call_args.args[0]['row'], 'vinyl_curator')

    def test_canceled_playlist_delete_keeps_the_detail_open(self):
        from wcs.vinyl import context as vinyl_context
        command = 'RunPlugin(plugin://plugin.video.milionar/?action=vinyl_context&op=delete_playlist&kind=playlist&id=7)'
        dialog = self.dialog()
        with patch.object(context, 'open_menu', return_value=command), patch.object(
                vinyl_context, 'run', return_value=False), patch.object(detail, 'load') as load:
            detail.options(self.params, dialog)
        self.assertFalse(dialog.closed)
        load.assert_not_called()


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