# -*- coding: utf-8 -*-
"""Click action of every Home widget (docs/plan/milionar-library/150-widget-clicks.md): every option of every kind of
item leads to the action its label promises, every widget row keeps working with every option."""
import os
import sys
import tempfile
import types
import unittest
from datetime import date, timedelta
from urllib.parse import parse_qsl, urlsplit

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
import xbmcplugin  # noqa: E402

from wcs.library2 import clicks, ctx, listings, meta, rows, store  # noqa: E402

TODAY = date(2026, 6, 15)
META = {'title': 'Titul', 'original': 'Original', 'plot': 'Dej', 'poster': 'http://p'}


def iso(days):
    return (TODAY + timedelta(days=days)).isoformat()


def action_of(path):
    """(action, params) a click really starts: the `run` of a skin_run URL, or the plugin URL itself."""
    params = dict(parse_qsl(urlsplit(path).query))
    if params.get('action') == 'skin_run':
        params = dict(parse_qsl(params['run']))
    return params.get('action'), params


class RegistryTests(unittest.TestCase):
    def test_every_row_has_a_valid_default_and_every_option_has_a_label(self):
        for row, kind in clicks.ROW_KIND.items():
            self.assertIn(row, clicks.DEFAULT, row)
            self.assertIn(clicks.DEFAULT[row], clicks.OPTIONS[kind], row)
            self.assertEqual(clicks.resolve(row, 'nonsense'), clicks.DEFAULT[row])      # a stale value falls back
            self.assertEqual(clicks.resolve(row, ''), clicks.DEFAULT[row])
            for option in clicks.OPTIONS[kind]:
                self.assertTrue(clicks.KIND_LABELS.get(kind, {}).get(option) or clicks.LABELS.get(option), (kind, option))
        self.assertEqual(clicks.options('my_movies_genres'), ())                         # folders have no choice
        self.assertEqual(clicks.resolve('my_movies_added', 'play'), 'play')
        self.assertEqual(clicks.resolve('my_series_added', 'play'), 'folder')            # not an option of a series row

    def test_the_defaults_keep_todays_behaviour(self):
        self.assertEqual([clicks.default(r) for r in ('cont_movies', 'my_movies_progress', 'my_movies_added', 'cont_series',
                                                      'my_series_added', 'widget_tmdb_popular_movies', 'widget_mytv_channels')],
                         ['play', 'play', 'detail', 'play', 'folder', 'detail', 'live'])

    def test_option_ids_are_unique_per_kind_and_the_sets_are_complete(self):
        for kind, options in clicks.OPTIONS.items():
            self.assertEqual(len(options), len(set(options)), kind)
        self.assertGreaterEqual(len(clicks.OPTIONS['episode']), 11)
        self.assertEqual(clicks.OPTIONS['tv_channel'], ('live', 'live_shuffle', 'first', 'random', 'open'))


class PathTests(unittest.TestCase):
    def test_movie_options(self):
        expect = {'play': ('mplay_req', {'type': 'movie', 'tmdb': '7', 'src': 'known', 'from': 'resume'}),
                  'play_start': ('mplay_req', {'src': 'known', 'from': 'start'}),
                  'stream': ('mplay_req', {'src': 'dialog', 'from': 'resume'}),
                  'detail': ('show_wcs_details', {'media_type': 'movie', 'tmdb_id': '7'}),
                  'similar': ('display_similar_movies_dialog', {'tmdb_id': '7', 'original_title': 'Original'}),
                  'game': ('milionar_game', {'media_type': 'movie', 'tmdb_id': '7', 'title': 'Titul', 'plot': 'Dej'}),
                  'list': ('ctx_list_toggle', {'type': 'movie', 'tmdb': '7'}),
                  'channel': ('ctx_channel', {'type': 'movie', 'tmdb': '7'}),
                  'watched': ('ctx_mark_toggle', {'type': 'movie', 'tmdb': '7'})}
        for option in clicks.OPTIONS['movie']:
            path, folder = clicks.title_path('movie', 7, option, meta=META)
            action, params = action_of(path)
            want_action, want = expect[option]
            self.assertEqual(action, want_action, option)
            for key, value in want.items():
                self.assertEqual(params.get(key), value, (option, key))
            self.assertFalse(folder, option)

    def test_series_options(self):
        expect = {'detail': ('show_wcs_details', {'media_type': 'tv', 'tmdb_id': '9'}),
                  'next': ('ctx_next', {'tmdb': '9', 'src': 'known', 'from': 'resume'}),
                  'next_stream': ('ctx_next', {'tmdb': '9', 'src': 'dialog'}),
                  'similar': ('display_similar_series_dialog', {'tmdb_id': '9'}),
                  'game': ('milionar_game', {'media_type': 'tv', 'tmdb_id': '9'}),
                  'list': ('ctx_list_toggle', {'type': 'tv', 'tmdb': '9'}),
                  'channel': ('ctx_channel', {'type': 'tv', 'tmdb': '9'}),
                  'watched': ('ctx_mark_toggle', {'type': 'tv', 'tmdb': '9'})}
        for option in clicks.OPTIONS['series']:
            path, folder = clicks.title_path('tv', 9, option, meta=META)
            if option == 'folder':
                self.assertEqual((action_of(path)[0], folder), ('mlist', True))
                self.assertEqual(action_of(path)[1]['kind'], 'seasons')
                continue
            action, params = action_of(path)
            self.assertEqual(action, expect[option][0], option)
            for key, value in expect[option][1].items():
                self.assertEqual(params.get(key), value, (option, key))
            self.assertFalse(folder)

    def test_episode_options(self):
        for option in clicks.OPTIONS['episode']:
            path, folder = clicks.title_path('episode', 9, option, 2, 5, META)
            action, params = action_of(path)
            if option in ('play', 'play_start', 'stream'):
                self.assertEqual((action, params['type'], params['s'], params['e']), ('mplay_req', 'episode', '2', '5'))
                self.assertEqual(params['src'], 'dialog' if option == 'stream' else 'known')
                self.assertEqual(params['from'], 'start' if option == 'play_start' else 'resume')
            elif option == 'detail':
                self.assertEqual((action, params['media_type']), ('show_wcs_details', 'tv'))     # the series' details
            elif option == 'folder':
                self.assertEqual((action, params['kind'], folder), ('mlist', 'seasons', True))
            elif option == 'season':
                self.assertEqual((action, params['kind'], params['s'], folder), ('mlist', 'episodes', '2', True))
            elif option == 'game':
                self.assertEqual((action, params['season_number'], params['episode_number']), ('milionar_game', '2', '5'))
            elif option == 'watched':
                self.assertEqual((action, params['type'], params['s'], params['e']), ('ctx_mark_toggle', 'episode', '2', '5'))
            elif option == 'list':
                self.assertEqual((action, params['type']), ('ctx_list_toggle', 'tv'))
            elif option == 'channel':
                self.assertEqual((action, params['type']), ('ctx_channel', 'tv'))
            elif option == 'similar':
                self.assertEqual(action, 'display_similar_series_dialog')
            else:
                self.fail(option)
            if option not in ('folder', 'season'):
                self.assertFalse(folder, option)

    def test_a_series_in_a_mixed_widget_plays_its_next_episode(self):
        for option, src in (('play', 'known'), ('play_start', 'known'), ('stream', 'dialog')):
            action, params = action_of(clicks.title_path('tv', 9, option, meta=META)[0])
            self.assertEqual((action, params['src']), ('ctx_next', src), option)
        action, params = action_of(clicks.title_path('tv', 9, 'play_start', meta=META)[0])
        self.assertEqual(params['from'], 'start')

    def test_an_unaired_episode_only_shows_its_date_for_the_playing_options(self):
        for option in clicks.UNAIRED_NOTE:
            self.assertEqual(action_of(clicks.title_path('episode', 9, option, 1, 3, META, unaired_text='x')[0])[0], 'mupcoming')
        self.assertEqual(action_of(clicks.title_path('episode', 9, 'detail', 1, 3, META, unaired_text='x')[0])[0], 'show_wcs_details')

    def test_an_unknown_option_never_breaks_a_widget(self):
        self.assertEqual(action_of(clicks.title_path('movie', 7, 'bogus', meta=META)[0])[0], 'show_wcs_details')

    def test_entries_of_the_cached_widgets(self):
        entry = {'tmdb_id': 5, 'media_type': 'series', 'title': 'S', 'plot': 'p', 'poster': 'x'}
        self.assertEqual(action_of(clicks.entry_path(entry, 'detail')[0])[1]['media_type'], 'tv')
        self.assertEqual(action_of(clicks.entry_path(dict(entry, media_type='movie'), 'play')[0])[0], 'mplay_req')


class ExecTests(unittest.TestCase):
    """What the skin's onclick runs (Kodi's own click would show its resume prompt for items with a resume point)."""

    def test_exec_for_every_kind_of_path(self):
        run, _folder = clicks.title_path('movie', 7, 'play', meta=META)
        self.assertEqual(clicks.exec_for(run), 'RunPlugin(plugin://plugin.video.milionar?action=mplay_req&type=movie&tmdb=7&src=known&from=resume)')
        path, folder = clicks.title_path('tv', 9, 'folder', meta=META)
        self.assertEqual(clicks.exec_for(path, folder), 'ActivateWindow(Videos,plugin://plugin.video.milionar/?action=mlist&kind=seasons&tmdb=9,return)')
        note, _f = clicks.title_path('episode', 9, 'play', 1, 3, META, unaired_text='Serial S01E03 vyjde 20. 6.')
        self.assertTrue(clicks.exec_for(note).startswith('RunPlugin(plugin://plugin.video.milionar/?action=mupcoming&text='))
        self.assertEqual(clicks.exec_for('plugin://x/?action=kraska_browse_path&sc_path=a%2Fb', True),
                         'ActivateWindow(Videos,plugin://x/?action=kraska_browse_path&sc_path=a%2Fb,return)')

    def test_no_exec_contains_a_raw_comma_that_would_split_the_builtin(self):
        meta = {'title': 'Titul, s carkou', 'original': 'Orig, x', 'plot': 'a, b', 'poster': 'http://p/x,y.jpg'}
        for kind in ('movie', 'tv', 'episode'):
            for option in clicks.OPTIONS['episode' if kind == 'episode' else 'series' if kind == 'tv' else 'movie']:
                path, folder = clicks.title_path(kind, 5, option, 1, 2, meta)
                command = clicks.exec_for(path, folder)
                inner = command[command.index('(') + 1:-1]
                if command.startswith('ActivateWindow'):
                    inner = inner.rsplit(',return', 1)[0].split(',', 1)[1]
                self.assertNotIn(',', inner, (kind, option))


class ExecTests(unittest.TestCase):
    """What the skin's onclick runs (Kodi's own click would show its resume prompt for items with a resume point)."""

    def test_exec_for_every_kind_of_path(self):
        run, _folder = clicks.title_path('movie', 7, 'play', meta=META)
        self.assertEqual(clicks.exec_for(run), 'RunPlugin(plugin://plugin.video.milionar?action=mplay_req&type=movie&tmdb=7&src=known&from=resume)')
        path, folder = clicks.title_path('tv', 9, 'folder', meta=META)
        self.assertEqual(clicks.exec_for(path, folder), 'ActivateWindow(Videos,plugin://plugin.video.milionar/?action=mlist&kind=seasons&tmdb=9,return)')
        note, _f = clicks.title_path('episode', 9, 'play', 1, 3, META, unaired_text='Serial S01E03 vyjde 20. 6.')
        self.assertTrue(clicks.exec_for(note).startswith('RunPlugin(plugin://plugin.video.milionar/?action=mupcoming&text='))
        self.assertEqual(clicks.exec_for('plugin://x/?action=kraska_browse_path&sc_path=a%2Fb', True),
                         'ActivateWindow(Videos,plugin://x/?action=kraska_browse_path&sc_path=a%2Fb,return)')

    def test_no_exec_contains_a_raw_comma_that_would_split_the_builtin(self):
        meta = {'title': 'Titul, s carkou', 'original': 'Orig, x', 'plot': 'a, b', 'poster': 'http://p/x,y.jpg'}
        for kind in ('movie', 'tv', 'episode'):
            for option in clicks.OPTIONS['episode' if kind == 'episode' else 'series' if kind == 'tv' else 'movie']:
                path, folder = clicks.title_path(kind, 5, option, 1, 2, meta)
                command = clicks.exec_for(path, folder)
                inner = command[command.index('(') + 1:-1]
                if command.startswith('ActivateWindow'):
                    inner = inner.rsplit(',return', 1)[0].split(',', 1)[1]
                self.assertNotIn(',', inner, (kind, option))


class RowClickTests(unittest.TestCase):
    """Every option of every library row, through the real row builders and the store."""

    def setUp(self):
        store.set_path(os.path.join(tempfile.mkdtemp(prefix='milionar-db-'), 'library.db'))
        for table in (xbmcplugin.ITEMS, xbmcplugin.ENDED, xbmcplugin.CONTENT, xbmcplugin.SORTS):
            table.clear()
        self._date = rows.date
        rows.date = type('D', (), {'today': staticmethod(lambda: TODAY)})
        meta.put(meta.key_movie(1), {'id': 1, 'title': 'Alfa', 'original_title': 'Alpha', 'release_date': '2020-01-01',
                                     'poster_path': '/p.jpg', 'genres': []}, 10 ** 9)
        store.set_in_list('movie', 1, True, {'id': 1, 'title': 'Alfa'})
        meta.put(meta.key_tv(10), {'id': 10, 'name': 'Show', 'original_name': 'Showy', 'status': 'Returning Series',
                                   'first_air_date': '2020-01-01', 'poster_path': '/s.jpg', 'genres': [],
                                   'seasons': [{'season_number': 1}]}, 10 ** 9)
        meta.put(meta.key_season(10, 1), {'episodes': [
            {'episode_number': 1, 'name': 'E1', 'air_date': iso(-30)}, {'episode_number': 2, 'name': 'E2', 'air_date': iso(-20)},
            {'episode_number': 3, 'name': 'E3', 'air_date': iso(9)}]}, 10 ** 9)
        store.set_in_list('tv', 10, True, {'id': 10, 'name': 'Show'})
        store.mark_watched('tv', 10, 1, 1, when=400)
        store.save_resume('movie', 1, 600, 6000, when=100)

    def tearDown(self):
        rows.date = self._date

    def build(self, row, click):
        return rows.build(row, {'row': row, 'click': click})

    def test_every_item_carries_what_the_skin_runs_on_click(self):
        for row in clicks.ROW_KIND:
            if row.startswith(('widget_', 'vinyl_', 'fy_')):     # Pro tebe rows: test_foryou.py
                continue
            for option in clicks.options(row):
                for path, item, folder in self.build(row, option):
                    command = item.getProperty('milionar.exec')
                    self.assertTrue(command, (row, option))
                    self.assertEqual(command.startswith('ActivateWindow(Videos,'), folder, (row, option))
                    self.assertTrue(command.startswith('ActivateWindow(Videos,') or command.startswith('RunPlugin(plugin://'), command)
        play = self.build('my_movies_progress', 'play')[0][1].getProperty('milionar.exec')
        self.assertIn('action=mplay_req', play)                       # in progress: Kodi's resume prompt is bypassed
        self.assertNotIn('skin_run', play)
        genres = rows.build('my_movies_genres', {'row': 'my_movies_genres'})
        self.assertEqual(genres, [] if not genres else genres)        # genre items are folders: Kodi opens them itself

    def test_every_movie_row_with_every_option(self):
        for row in ('cont_movies', 'my_movies_progress', 'my_movies_added', 'my_movies_unwatched', 'my_movies_watched', 'my_movies_all'):
            for option in clicks.options(row):
                for path, item, folder in self.build(row, option):
                    self.assertEqual(item.getProperty('milionar.type'), 'movie')            # the context menu stays
                    self.assertEqual(item.getProperty('IsPlayable'), 'true', (row, option))
                    self.assertFalse(folder)
                    self.assertTrue(action_of(path)[0], (row, option))
        path = self.build('my_movies_all', 'similar')[0][0]
        self.assertEqual(action_of(path)[1]['original_title'], 'Alpha')                   # the original title travels

    def test_series_rows_with_every_option_and_folders_stay_folders(self):
        for row in ('my_series_added', 'my_series_unwatched', 'my_series_all'):
            for option in clicks.options(row):
                for path, item, folder in self.build(row, option):
                    self.assertEqual(folder, option == 'folder', (row, option))
                    self.assertEqual(item.getProperty('IsPlayable') == 'true', not folder, (row, option))

    def test_episode_rows_with_every_option_and_the_unaired_one_keeps_its_note(self):
        for row in ('cont_series', 'my_series_next', 'my_series_new', 'my_series_upcoming'):
            for option in clicks.options(row):
                for path, item, folder in self.build(row, option):
                    self.assertEqual(folder, option in ('folder', 'season'), (row, option))
                    if option in clicks.UNAIRED_NOTE and 'vyjde' in item.label:
                        self.assertEqual(action_of(path)[0], 'mupcoming', (row, option))
        self.assertEqual(action_of(self.build('my_series_upcoming', 'play')[0][0])[0], 'mupcoming')
        self.assertEqual(action_of(self.build('my_series_upcoming', 'detail')[0][0])[0], 'show_wcs_details')

    def test_a_stored_value_that_does_not_fit_the_row_uses_the_default(self):
        self.assertEqual(action_of(self.build('my_movies_all', 'folder')[0][0])[0], 'show_wcs_details')       # movie: detail
        self.assertEqual(self.build('my_series_all', 'play')[0][2], True)                                       # series: folder
        self.assertEqual(action_of(self.build('cont_movies', '')[0][0])[0], 'mplay_req')                      # unset = default

    def test_recent_played_is_mixed(self):
        store.log_play('movie', 1, None, None, 500, 600, 600, 6000, 'resume')
        store.log_play('tv', 10, 1, 2, 700, 800, 300, 2400, 'resume')
        for option in clicks.options('recent_played'):
            built = self.build('recent_played', option)
            self.assertEqual(len(built), 2, option)                                      # a film and an episode
            for path, item, folder in built:
                self.assertFalse(folder)
                self.assertTrue(action_of(path)[0], option)


class ListClickTests(unittest.TestCase):
    """Moje filmy / Moje seriály: the skin's click (skin=1) never reads the addon setting, the classic menu still does."""

    def setUp(self):
        store.set_path(os.path.join(tempfile.mkdtemp(prefix='milionar-db-'), 'library.db'))
        for table in (xbmcplugin.ITEMS, xbmcplugin.ENDED, xbmcplugin.CONTENT, xbmcplugin.SORTS):
            table.clear()
        xbmcaddon.Addon.settings.clear()
        meta.put(meta.key_movie(1), {'id': 1, 'title': 'Alfa', 'original_title': 'Alpha', 'release_date': '2020-01-01',
                                     'poster_path': '/p.jpg', 'genres': []}, 10 ** 9)
        store.set_in_list('movie', 1, True, {'id': 1, 'title': 'Alfa'})
        meta.put(meta.key_tv(10), {'id': 10, 'name': 'Show', 'status': 'Ended', 'first_air_date': '2020-01-01', 'genres': [],
                                   'seasons': [{'season_number': 1}]}, 10 ** 9)
        store.set_in_list('tv', 10, True, {'id': 10, 'name': 'Show'})
        self.skin, self._info = {}, xbmc.getInfoLabel
        xbmc.getInfoLabel = lambda label: self.skin.get(label, '')
        self.handle = 100

    def tearDown(self):
        xbmc.getInfoLabel = self._info

    def listing(self, kind, **params):
        self.handle += 1
        listings.run(self.handle, dict(params, kind=kind))
        return [(p, i, f) for p, i, f in xbmcplugin.ITEMS[self.handle] if i.getProperty('milionar.type')]

    def test_the_movie_list_follows_the_skin_setting(self):
        for option in clicks.LIST_OPTIONS['movies']:
            self.skin['Skin.String(mil.list.movies.click)'] = option
            (path, item, folder), = self.listing('movies', skin='1')
            self.assertEqual(action_of(path)[0], 'show_wcs_details' if option == 'detail' else 'mplay_req', option)
            self.assertEqual((item.getProperty('IsPlayable'), folder), ('true', False))
        self.skin.clear()                                                                   # unset: the skin default
        xbmcaddon.Addon.settings['default_movie_action'] = '1'                              # the addon setting is ignored
        self.assertEqual(action_of(self.listing('movies', skin='1')[0][0])[0], 'show_wcs_details')
        self.skin['Skin.String(mil.list.movies.click)'] = 'folder'                          # not an option of a movie list
        self.assertEqual(action_of(self.listing('movies', skin='1')[0][0])[0], 'show_wcs_details')

    def test_the_classic_menu_keeps_the_addon_setting(self):
        self.skin['Skin.String(mil.list.movies.click)'] = 'stream'
        xbmcaddon.Addon.settings['default_movie_action'] = '0'
        self.assertEqual(action_of(self.listing('movies')[0][0])[0], 'show_wcs_details')
        xbmcaddon.Addon.settings['default_movie_action'] = '1'
        self.assertEqual(action_of(self.listing('movies')[0][0])[0], 'mplay')                # Kodi's own play of the canonical url

    def test_the_series_list_follows_the_skin_setting(self):
        for option in clicks.LIST_OPTIONS['series']:
            self.skin['Skin.String(mil.list.series.click)'] = option
            (path, item, folder), = self.listing('series', skin='1')
            self.assertEqual(folder, option == 'folder', option)
            self.assertEqual(item.getProperty('IsPlayable') == 'true', option != 'folder', option)
            action = action_of(path)[0]
            self.assertEqual(action, {'folder': 'mlist', 'detail': 'show_wcs_details'}.get(option, 'ctx_next'), option)
        self.skin.clear()
        self.assertEqual(self.listing('series', skin='1')[0][2], True)                       # unset: the seasons
        self.assertEqual(self.listing('series')[0][2], True)                                 # classic menu: the seasons


class CtxActionTests(unittest.TestCase):
    def setUp(self):
        store.set_path(os.path.join(tempfile.mkdtemp(prefix='milionar-db-'), 'library.db'))
        self._date = rows.date
        rows.date = type('D', (), {'today': staticmethod(lambda: TODAY)})
        self.notes, self._notification = [], xbmcgui.Dialog.notification
        xbmcgui.Dialog.notification = lambda dialog, heading, message, *a, **k: self.notes.append(message)
        xbmcaddon.Addon.strings.update({32398: 'No next', 32418: 'Added {}', 32419: 'Removed {}', 32351: 'Movies', 32352: 'Series'})

    def tearDown(self):
        rows.date = self._date
        xbmcgui.Dialog.notification = self._notification

    def show(self, tmdb_id, episodes, status='Returning Series', in_list=True):
        meta.put(meta.key_tv(tmdb_id), {'id': tmdb_id, 'name': 'S{}'.format(tmdb_id), 'status': status, 'genres': [],
                                        'seasons': [{'season_number': 1}]}, 10 ** 9)
        meta.put(meta.key_season(tmdb_id, 1), {'episodes': [
            {'episode_number': i + 1, 'name': 'E', 'air_date': iso(d)} for i, d in enumerate(episodes)]}, 10 ** 9)
        if in_list:
            store.set_in_list('tv', tmdb_id, True, {'id': tmdb_id, 'name': 'S{}'.format(tmdb_id)})

    def test_next_episode_target(self):
        self.show(10, [-30, -20, -10])
        store.mark_watched('tv', 10, 1, 1, when=1)
        self.assertEqual(ctx.next_target(rows.Data(), 10), (1, 2))                        # the next one
        store.save_resume('tv', 10, 300, 2400, 1, 2, when=5)
        self.assertEqual(ctx.next_target(rows.Data(), 10), (1, 2))                        # the one in progress
        self.show(11, [-30, -20])
        self.assertEqual(ctx.next_target(rows.Data(), 11), (1, 1))                        # not started: the first
        self.assertEqual(ctx.next_target(rows.Data(), 999), (1, 1))                       # unknown to the cache: S01E01
        self.show(12, [-30, 5])
        store.mark_watched('tv', 12, 1, 1, when=1)
        self.assertIsNone(ctx.next_target(rows.Data(), 12))                               # the next has not aired

    def test_play_next_requests_the_episode_or_says_there_is_none(self):
        from wcs.library2 import play
        requests, original = [], play.play_episode
        play.play_episode = lambda *args, **kwargs: requests.append(args)
        try:
            self.show(10, [-30, -20])
            store.mark_watched('tv', 10, 1, 1, when=1)
            ctx.play_next(10, 'known', 'resume')
            ctx.play_next(10, 'dialog', 'resume')
            self.show(12, [-30, 5])
            store.mark_watched('tv', 12, 1, 1, when=1)
            ctx.play_next(12)
        finally:
            play.play_episode = original
        self.assertEqual(requests, [(10, 1, 2, 'known', 'resume'), (10, 1, 2, 'dialog', 'resume')])
        self.assertEqual(self.notes, ['No next'])

    def test_list_and_watched_toggles(self):
        meta.put(meta.key_movie(3), {'id': 3, 'title': 'Film', 'genres': []}, 10 ** 9)
        ctx.run({'action': 'ctx_list_toggle', 'type': 'movie', 'tmdb': '3'})
        self.assertTrue((rows.Data().titles('movie').get(3) or {}).get('in_list'))
        ctx.run({'action': 'ctx_list_toggle', 'type': 'movie', 'tmdb': '3'})
        self.assertFalse((rows.Data().titles('movie').get(3) or {}).get('in_list'))
        ctx.run({'action': 'ctx_mark_toggle', 'type': 'movie', 'tmdb': '3'})
        self.assertEqual(int(store.get_state('movie', 3)['playcount']), 1)
        ctx.run({'action': 'ctx_mark_toggle', 'type': 'movie', 'tmdb': '3'})
        self.assertEqual(int((store.get_state('movie', 3) or {}).get('playcount') or 0), 0)
        self.show(10, [-30, -20])
        ctx.run({'action': 'ctx_mark_toggle', 'type': 'episode', 'tmdb': '10', 's': '1', 'e': '2'})
        self.assertEqual(int(store.get_state('tv', 10, 1, 2)['playcount']), 1)


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