# -*- coding: utf-8 -*-
"""Targeted detail tests: cache-only rows, playback safety and navigation rollback."""
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
import xml.etree.ElementTree as ET

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent / 'library_harness'))
import stubs
stubs.install()
sys.path.insert(0, str(HERE))
import gen_detail
import gen_cinema
import xbmc
import xbmcgui
import xbmcplugin
from wcs.library2 import clicks, meta, store, tmdb
from wcs.skin import detail


def movie(tmdb_id=1, **extra):
    return dict({'id': tmdb_id, 'title': 'Film {}'.format(tmdb_id), 'genres': [], 'runtime': 120,
                 'overview': 'A plot', 'credits': {'cast': []}, 'videos': {'results': []},
                 'recommendations': {'results': []}, 'similar': {'results': []}}, **extra)


def show(**extra):
    return dict(movie(), name='Series', number_of_seasons=1, seasons=[{'season_number': 1, 'episode_count': 2}], **extra)


class DetailTests(unittest.TestCase):
    def test_hero_click_is_available_only_in_cinematic_widget_settings(self):
        for section in clicks.HERO_SECTIONS:
            for row in ('cont_movies', 'cont_series', 'widget_mytv_channels', 'widget_mytv_next'):
                self.assertIn('hero', clicks.options(row, section))
                self.assertEqual(clicks.resolve(row, 'hero'), 'hero')
        self.assertNotIn('hero', clicks.options('widget_mytv_channels', 'mycinema'))
        self.assertIn('hero', clicks.options('vinyl_songs', 'vinyl'))
        self.assertNotIn('hero', clicks.options('vinyl_songs', 'mycinema'))
        self.assertNotIn('hero', clicks.options('my_movies_genres', 'my_movies'))

    def setUp(self):
        self.temp = tempfile.TemporaryDirectory()
        store.set_path(os.path.join(self.temp.name, 'library.db'))
        xbmcgui.Window.props.clear()
        meta.put(meta.key_movie(1), movie(), meta.TTL_MOVIE)
        self.focus = {'window': 10000, 'control': 29100, 'position': 4, 'row': ''}
        self.patches = [patch.object(detail, '_focus', side_effect=lambda: dict(self.focus)),
                        patch.object(xbmcgui, 'getCurrentWindowId', return_value=10000, create=True),
                        patch.object(xbmcgui, 'NOTIFICATION_ERROR', 'error', create=True),
                        patch.object(xbmc, 'getCondVisibility', return_value=True),
                        patch.object(xbmc, 'executebuiltin')]
        self.mocks = [p.start() for p in self.patches]
        self.commands = self.mocks[-1]
        self.network = patch.object(tmdb, '_fetch', side_effect=AssertionError('unexpected network')).start()

    def tearDown(self):
        patch.stopall()
        self.temp.cleanup()

    def open(self, tmdb_id=1, media='movie'):
        detail.open_detail({'media_type': media, 'tmdb_id': str(tmdb_id)})

    def test_complete_detail_uses_canonical_cache_without_network(self):
        self.assertEqual(tmdb.detail('movie', 1)['title'], 'Film 1')
        self.network.assert_not_called()

    def test_missing_appended_fields_fetches_once_and_reuses_the_same_key(self):
        meta.put(meta.key_movie(2), {'id': 2, 'genres': []}, 100)
        fetched = movie(2)
        result = tmdb.detail('movie', 2, fetch=lambda path, params: fetched)
        self.assertEqual(meta.get(meta.key_movie(2))[0], result)
        tmdb.detail('movie', 2)
        self.network.assert_not_called()

    def test_related_cards_and_actor_rows_never_fetch_or_change_the_pinned_title(self):
        meta.put(meta.key_movie(1), movie(recommendations={'results': [{'id': 2, 'title': 'Related', 'poster_path': '/p.jpg'}]},
                                         credits={'cast': [{'id': 9, 'name': 'Actor', 'character': 'Role'}]}), 100)
        self.open()
        title = detail._get('title')
        state = detail._state()
        self.assertEqual(detail.items(state, 'recommendations')[0][1].getLabel(), 'Related')
        self.assertEqual(len(detail.items(state, 'cast')), 1)
        self.assertEqual(detail._get('title'), title)
        self.network.assert_not_called()

    def test_recommendations_and_similar_exclude_opened_title_and_duplicates(self):
        meta.put(meta.key_movie(1), movie(recommendations={'results': [{'id': 1}, {'id': 2}, {'id': 2}]},
                                         similar={'results': [{'id': 2}, {'id': 3}, {'id': 3}]}), 100)
        state = {'media': 'movie', 'id': 1, 'season': None}
        self.assertEqual([e['id'] for e in detail._entries(state, 'recommendations')], [2])
        self.assertEqual([e['id'] for e in detail._entries(state, 'similar')], [3])

    def test_collection_is_chronological_and_absent_collection_is_hidden(self):
        self.open()
        self.assertEqual(detail._get('has.collection'), '')
        meta.put(meta.key_movie(1), movie(belongs_to_collection={'id': 10}), 100)
        meta.put(meta.key_collection(10), {'parts': [{'id': 3, 'release_date': '2020'}, {'id': 2, 'release_date': '2000'}]}, 100)
        self.open()
        self.assertEqual([e['id'] for e in detail._entries(detail._state(), 'collection')], [2, 3])

    def test_nested_back_restores_title_row_and_selected_position_then_origin(self):
        meta.put(meta.key_movie(2), movie(2), 100)
        self.open()
        self.focus.update(control=41500, position=3, row='41500')
        self.open(2)
        detail.back()
        self.assertEqual(detail._state()['id'], 1)
        self.assertEqual(detail._home().getProperty('milionar.row.detail'), '41500')
        self.commands.assert_any_call('SetFocus(41500,3,absolute)', True)
        detail.back()
        self.assertEqual(detail._get('active'), '')
        self.commands.assert_any_call('SetFocus(29100,4,absolute)', True)

    def test_failed_open_preserves_previous_title_and_stack(self):
        self.open()
        state, stack = detail._get('state'), detail._get('stack')
        self.open(99)
        self.assertEqual((detail._get('state'), detail._get('stack')), (state, stack))

    def test_failed_season_preserves_previous_view(self):
        meta.put(meta.key_tv(1), show(), 100)
        self.open(media='tv')
        state = detail._get('state')
        detail.season(1)
        self.assertEqual(detail._get('state'), state)

    def test_season_back_and_unaired_episode_does_not_play(self):
        meta.put(meta.key_tv(1), show(), 100)
        meta.put(meta.key_season(1, 1), {'episodes': [
            {'episode_number': 1, 'name': 'Aired', 'air_date': '2000-01-01'},
            {'episode_number': 2, 'name': 'Future', 'air_date': '2099-01-01'}]}, 100)
        self.open(media='tv')
        self.focus.update(control=41100, position=0, row='41100')
        detail.season(1)
        items = detail.items(detail._state(), 'episodes')
        self.assertIn('mplay_req', items[0][1].getProperty('milionar.exec'))
        self.assertIn('mupcoming', items[1][1].getProperty('milionar.exec'))
        detail.back()
        self.assertIsNone(detail._state()['season'])
        self.commands.assert_any_call('SetFocus(41100,0,absolute)', True)

    def test_old_row_request_reads_url_identity_even_after_navigation(self):
        meta.put(meta.key_movie(2), movie(2, credits={'cast': [{'id': 9, 'name': 'Old actor'}]}), 100)
        self.open()
        with patch.object(xbmcplugin, 'addDirectoryItems') as add:
            detail.row(1, {'key': 'movie:2:all', 'kind': 'cast'})
        self.assertEqual(add.call_args.args[1][0][1].getLabel(), 'Old actor')
        self.assertEqual(detail._get('title'), 'Film 1')
        self.network.assert_not_called()

    def test_menu_exit_clears_stack_without_stealing_sidebar_focus(self):
        self.open()
        self.commands.reset_mock()
        detail.back(root=True, menu=True)
        self.assertEqual(detail._get('active'), '')
        self.commands.assert_not_called()

    def test_generated_xml_is_current_and_shared_references_resolve(self):
        generated = gen_detail.build()
        self.assertEqual(gen_detail.OUT.read_text(), generated)
        root = ET.fromstring(generated)
        shared = ET.fromstring(gen_cinema.build())
        names = {node.get('name') for tree in (root, shared) for node in tree.findall('include')}
        for node in root.iter('include'):
            if node.get('content') and '$PARAM' not in node.get('content'):
                self.assertIn(node.get('content'), names)
        self.assertIn('<include>MilionarDetail</include>', (gen_detail.OUT.parent / 'Home.xml').read_text())


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