# -*- coding: utf-8 -*-
"""Offline tests of the Millionaire demo: data, moderator, whole games through the real controller and game panel,
lifelines, and the wiring into the Milionář widgets.

Run: python3 tests/test_millionaire_demo.py        (no Kodi, no network, no AI: any socket use fails the test)
"""
import json
import os
import re
import socket
import sys
import threading
import time
import unittest

sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import kodi_stubs                                           # noqa: E402

kodi_stubs.install()

ADDON = kodi_stubs.ADDON_DIR
REAL_SLEEP = time.sleep

from wcs.games import designs                           # noqa: E402
from wcs.games.demo import catalog, moderator          # noqa: E402
from wcs.games.content.demo import DemoContent         # noqa: E402
from wcs.games.MillionaireGame import MillionaireGameController  # noqa: E402

MONEY = ['1 000 Kč', '2 000 Kč', '5 000 Kč', '10 000 Kč', '20 000 Kč', '50 000 Kč', '100 000 Kč', '500 000 Kč',
         '1 000 000 Kč']


def setUpModule():
    time.sleep = lambda seconds: REAL_SLEEP(0)               # the game's countdowns and pauses run instantly

    def no_network(*args, **kwargs):
        raise AssertionError('the offline demo tried to use the network')
    socket.socket.connect = no_network


def tearDownModule():
    time.sleep = REAL_SLEEP


def wait_for(condition, what, timeout=5.0):
    deadline = REAL_SLEEP and time.monotonic() + timeout
    while time.monotonic() < deadline:
        if condition():
            return
        REAL_SLEEP(0.002)
    raise AssertionError('timed out waiting for ' + what)


class Screen:
    """What the controller's UI callbacks received (the role of the game panel)."""

    def __init__(self):
        self.texts, self.buttons, self.colors, self.closed = [], [], [], False
        self.lock = threading.Lock()

    def display(self, text):
        self.texts.append(text)

    def set_buttons(self, *labels):
        self.buttons.append(labels)

    def answer_colors(self, selected, correct=None):
        self.colors.append((selected, correct))

    def close(self):
        self.closed = True

    def last_buttons(self):
        return self.buttons[-1] if self.buttons else ()


def make_game(title, content=None):
    screen = Screen()
    game = MillionaireGameController(title=title.title, plot=title.plot, genre=title.genre,
                                     media_type=title.media, content=content or DemoContent(title, pace=0))
    game.set_ui_callbacks(update_display=screen.display, update_buttons=screen.set_buttons,
                          update_sidebar=lambda: None, show_error=lambda message: screen.texts.append(message),
                          set_focus=lambda index: None, start_countdown=None, close_dialog=screen.close,
                          update_millionaire_ladder=lambda: None, set_answer_button_colors=screen.answer_colors)
    game.set_return_to_menu_callback(screen.close)
    return game, screen


def start_game(game):
    """From the welcome menu through "Začít hru" and the moderator's welcome to the first question."""
    game.start_specific_game()
    assert game.game_state == 'millionaire_welcome'
    game.handle_game_choice(0)
    wait_for(lambda: game.game_state == 'welcome_screen', 'welcome screen')
    game.handle_game_choice(0)
    wait_for(lambda: game.game_state == 'playing', 'first question')


def phase_three_done(screen, before):
    """The action buttons after an answer (Další otázka / Výsledky / Nová hra) were shown."""
    return len(screen.buttons) > before and screen.last_buttons()[:1] in (('Další otázka',), ('Výsledky',), ('Nová hra',))


def answer(game, screen, index):
    before = len(screen.buttons)
    game.answer_question(index)
    wait_for(lambda: phase_three_done(screen, before), 'result of the answer')


def play_to_the_end(game, screen, wrong_at=None):
    """Answer every question right (or wrong in round `wrong_at`); returns the questions shown, in order."""
    asked = []
    for round_num in range(1, 10):
        assert game.game_state == 'playing', (round_num, game.game_state)
        assert game.current_round == round_num
        question = game.current_question
        asked.append(question)
        index = question['correct']
        if round_num == wrong_at:
            index = (index + 1) % 4
        answer(game, screen, index)
        if round_num == wrong_at:
            return asked
        if round_num < 9:
            assert game.game_state == 'answer_feedback'
            game.handle_game_choice(0)                       # Další otázka
            wait_for(lambda: game.game_state == 'playing' and game.current_round == round_num + 1, 'next question')
    return asked


class DataTests(unittest.TestCase):
    def test_pack_has_eighteen_films_and_eleven_series(self):
        self.assertEqual(len(catalog.by_media('movie')), 18)
        self.assertEqual(len(catalog.by_media('series')), 11)
        files = [n for n in os.listdir(os.path.join(catalog.PACK_DIR, 'titles')) if n.endswith('.json')]
        self.assertEqual(len(files), 29, 'a title file failed validation and was skipped')
        self.assertEqual([t.id for t in catalog.by_media('movie')][:5],
                         ['matrix', 'inception', 'forrest-gump', 'titanic', 'jurassic-park'])
        self.assertEqual([t.id for t in catalog.by_media('series')][:3], ['game-of-thrones', 'breaking-bad', 'friends'])

    def test_every_file_is_valid_and_titles_are_unique(self):
        ids, tmdb = set(), set()
        for name in sorted(os.listdir(os.path.join(catalog.PACK_DIR, 'titles'))):
            raw = catalog.read_json('titles', name)
            self.assertEqual(catalog.problems(raw), [], name)
            self.assertEqual(name, raw['id'] + '.json')
            ids.add(raw['id'])
            tmdb.add((raw['media'], raw['tmdb_id']))
            self.assertTrue(raw['poster'].startswith('/') and raw['fanart'].startswith('/'))
        self.assertEqual((len(ids), len(tmdb)), (29, 29))

    def test_harry_potter_collection_is_complete_and_contiguous_in_release_order(self):
        movies = catalog.by_media('movie')
        start = next(n for n, title in enumerate(movies) if title.id == 'harry-potter-1')
        collection = movies[start:start + 8]
        self.assertEqual([title.id for title in collection], ['harry-potter-' + str(n) for n in range(1, 9)])
        self.assertEqual([title.tmdb_id for title in collection], [671, 672, 673, 674, 675, 767, 12444, 12445])
        self.assertEqual([title.year for title in collection], [2001, 2002, 2004, 2005, 2007, 2009, 2010, 2011])

    def test_validator_rejects_broken_titles(self):
        good = catalog.read_json('titles', 'matrix.json')
        for change in ({'media': 'game'}, {'questions': good['questions'][:8]}, {'year': '1999'}):
            self.assertTrue(catalog.problems(dict(good, **change)), change)
        broken = json.loads(json.dumps(good))
        broken['questions'][0]['options'][1] = broken['questions'][0]['options'][0]
        broken['questions'][1]['correct'] = 4
        broken['questions'][2]['tip'] = ''
        text = ' '.join(catalog.problems(broken))
        for fragment in ('duplicates', 'correct', 'tip'):
            self.assertIn(fragment, text)

    def test_the_right_answer_is_not_always_the_same_letter(self):
        for title in catalog.titles():
            letters = {q.correct for q in title.questions}
            self.assertGreaterEqual(len(letters), 3, title.id)

    def test_moderator_has_a_line_for_every_title_round_and_result(self):
        for title in catalog.titles():
            lines = [moderator.welcome(title), moderator.rules(title)]
            for round_num in range(1, 10):
                lines.append(moderator.intro(title, round_num))
                for right in (True, False):
                    lines += [moderator.result(title, round_num, right), moderator.transition(title, round_num, right)]
            for line in lines:
                self.assertTrue(line.strip(), title.id)
                self.assertNotRegex(line, r'[{}]', title.id)
        title = catalog.get('matrix')
        self.assertIn(MONEY[0], moderator.intro(title, 1))
        self.assertIn(MONEY[8], moderator.intro(title, 9))
        self.assertIn(MONEY[4], moderator.transition(title, 4, True))
        self.assertEqual(moderator.intro(title, 3), moderator.intro(title, 3), 'a game always reads the same')

    def test_the_demo_never_loads_the_ai_client(self):
        self.assertNotIn('wcs.ai.AIHandler', sys.modules)


class ControllerGameTests(unittest.TestCase):
    def test_a_perfect_game_of_every_title(self):
        for title in catalog.titles():
            with self.subTest(title=title.id):
                game, screen = make_game(title)
                start_game(game)
                asked = play_to_the_end(game, screen)
                self.assertEqual([q['question'] for q in asked], [q.text for q in title.questions])
                self.assertEqual(game.game_state, 'game_finished')
                self.assertEqual(game.score, 9)
                self.assertEqual(screen.last_buttons()[0], 'Výsledky')
                joined = '\n'.join(screen.texts)
                for item in title.questions:
                    self.assertIn(item.explanation, joined)
                for money in MONEY:
                    self.assertIn(money, joined)
                game.handle_game_choice(0)                   # Výsledky
                self.assertIn('VÍTĚZSTVÍ', screen.texts[-1])

    def test_questions_are_shown_with_the_authored_options_and_money(self):
        title = catalog.get('titanic')
        game, screen = make_game(title)
        start_game(game)
        for round_num, item in enumerate(title.questions, 1):
            self.assertEqual(game.current_question['options'], list(item.options))
            self.assertEqual(game.current_question['correct'], item.correct)
            shown = [b for b in screen.buttons if b == tuple(item.options)]
            self.assertTrue(shown, 'round {}: the four answers were never put on the buttons'.format(round_num))
            self.assertIn(MONEY[round_num - 1], '\n'.join(screen.texts))
            answer(game, screen, item.correct)
            if round_num < 9:
                game.handle_game_choice(0)
                wait_for(lambda: game.current_round == round_num + 1 and game.game_state == 'playing', 'next round')

    def test_a_wrong_answer_ends_the_game_in_any_round(self):
        for round_num in range(1, 10):
            title = catalog.titles()[round_num % 20]
            with self.subTest(title=title.id, round=round_num):
                game, screen = make_game(title)
                start_game(game)
                asked = play_to_the_end(game, screen, wrong_at=round_num)
                self.assertEqual(len(asked), round_num)
                self.assertEqual(game.game_state, 'game_over')
                item = title.questions[round_num - 1]
                text = screen.texts[-1]
                self.assertIn('ŠPATNĚ', text)
                self.assertIn(item.options[item.correct], text)
                self.assertIn(item.explanation, text)
                self.assertEqual(screen.last_buttons()[0], 'Nová hra')
                self.assertIn((((item.correct + 1) % 4), item.correct), screen.colors)   # right green, chosen red

    def test_a_new_game_after_game_over_plays_the_same_questions(self):
        title = catalog.get('sherlock')
        game, screen = make_game(title)
        start_game(game)
        play_to_the_end(game, screen, wrong_at=2)
        game.handle_game_choice(0)                           # Nová hra
        wait_for(lambda: game.game_state == 'welcome_screen', 'welcome of the new game')
        game.handle_game_choice(0)
        wait_for(lambda: game.game_state == 'playing', 'first question of the new game')
        self.assertEqual(game.current_round, 1)
        self.assertEqual(game.current_question['question'], title.questions[0].text)
        self.assertFalse(game.lifeline_5050_used or game.lifeline_audience_used or game.lifeline_tip_used)

    def test_fifty_fifty_keeps_the_right_answer_and_one_wrong(self):
        for title in catalog.titles():
            with self.subTest(title=title.id):
                game, screen = make_game(title)
                start_game(game)
                game.use_5050_lifeline()
                kept = set(game.lifeline_5050_indices)
                self.assertEqual(len(kept), 2)
                self.assertIn(game.current_question['correct'], kept)
                labels = screen.last_buttons()
                self.assertEqual(sum(1 for label in labels if label), 2)
                removed = next(i for i in range(4) if i not in kept)
                game.answer_question(removed)                # a removed answer cannot be chosen
                self.assertEqual(game.game_state, 'playing')
                game.use_5050_lifeline()                     # and the lifeline works only once
                self.assertEqual(set(game.lifeline_5050_indices), kept)

    def test_audience_poll_adds_up_to_a_hundred_and_respects_fifty_fifty(self):
        title = catalog.get('friends')
        game, screen = make_game(title)
        start_game(game)
        game.use_audience_poll_lifeline()
        wait_for(lambda: 'VÝSLEDKY HLASOVÁNÍ' in screen.texts[-1] and 'Vyberte svou odpověď' in screen.texts[-1],
                 'the audience poll animation')
        self.assertEqual(sum(game.lifeline_audience_results), 100)
        self.assertTrue(game.lifeline_audience_used)
        before = list(game.lifeline_audience_results)
        game.use_audience_poll_lifeline()
        self.assertEqual(game.lifeline_audience_results, before, 'the lifeline works only once')

        game, screen = make_game(title)
        start_game(game)
        game.use_5050_lifeline()
        game.use_audience_poll_lifeline()
        kept = set(game.lifeline_5050_indices)
        self.assertEqual(sum(game.lifeline_audience_results), 100)
        self.assertTrue(all(game.lifeline_audience_results[i] == 0 for i in range(4) if i not in kept))

    def test_tip_shows_the_authored_tip_in_the_role_of_the_work(self):
        for title in catalog.titles():
            with self.subTest(title=title.id):
                game, screen = make_game(title)
                start_game(game)
                item = title.questions[0]
                game.use_tip_lifeline()
                wait_for(lambda: item.tip in '\n'.join(screen.texts[-3:]), 'the tip')
                self.assertIn('TIP OD ' + ('TVŮRCE' if title.media == 'series' else 'REŽISÉRA'), screen.texts[-1])
                self.assertEqual(screen.last_buttons(), tuple(item.options))
                self.assertTrue(game.lifeline_tip_used)

    def test_all_three_lifelines_in_one_game_and_the_game_still_finishes(self):
        title = catalog.get('breaking-bad')
        game, screen = make_game(title)
        start_game(game)
        game.use_5050_lifeline()
        answer(game, screen, game.current_question['correct'])
        game.handle_game_choice(0)
        wait_for(lambda: game.current_round == 2 and game.game_state == 'playing', 'round 2')
        game.use_audience_poll_lifeline()
        # the poll animation shows "Vyberte svou odpověď" a few frames before its last one: wait for the final frame
        wait_for(lambda: (game.poll_view or {}).get('final'), 'poll finished')
        game.use_tip_lifeline()
        wait_for(lambda: title.questions[1].tip in screen.texts[-1], 'tip shown')
        self.assertFalse(game.lifeline_5050_used and game.lifeline_5050_indices, '50:50 is spent per question')
        answer(game, screen, game.current_question['correct'])
        game.handle_game_choice(0)
        wait_for(lambda: game.current_round == 3 and game.game_state == 'playing', 'round 3')
        for round_num in range(3, 10):
            answer(game, screen, game.current_question['correct'])
            if round_num < 9:
                game.handle_game_choice(0)
                wait_for(lambda: game.current_round == round_num + 1 and game.game_state == 'playing', 'next round')
        self.assertEqual(game.game_state, 'game_finished')

    def test_rules_and_welcome_say_it_is_the_offline_demo(self):
        title = catalog.get('the-godfather')
        game, screen = make_game(title)
        game.start_specific_game()
        self.assertIn('OFFLINE DEMO', screen.texts[-1])
        self.assertIn('Kmotr', screen.texts[-1])
        game.handle_game_choice(1)                           # Pravidla
        self.assertIn('Demo: otázky jsou předpřipravené, bez AI', screen.texts[-1])
        self.assertNotIn('AI vytváří', screen.texts[-1])
        game.handle_game_choice(1)                           # Zpět
        self.assertEqual(game.game_state, 'millionaire_welcome')

    def test_the_moderator_is_silent(self):
        game, _screen = make_game(catalog.get('chernobyl'))
        self.assertFalse(game.voice.speaks_feedback())
        self.assertIsNone(game.voice.wait(60))

    def test_spectator_mode_plays_a_whole_demo_game_by_itself(self):
        title = catalog.get('stranger-things')
        game, screen = make_game(title)
        original = game._load_autoplay_settings

        def spectator():
            game._autoplay_enabled, game._autoplay_fail_round = True, 0
            game._autoplay_speed, game._autoplay_lifeline_freq = 0, 3
            game._autoplay_overridden = False
        game._load_autoplay_settings = spectator
        game.start_specific_game()
        game.handle_game_choice(0)
        wait_for(lambda: game.game_state == 'game_finished', 'the spectator game to finish', timeout=20)
        self.assertEqual(game.score, 9)
        game._load_autoplay_settings = original


class GamePanelTests(unittest.TestCase):
    """The real MilionarGameModal with the adapter: clicks and window properties, as in Kodi."""

    @classmethod
    def setUpClass(cls):
        from wcs.games.DialogMilionarGame import MilionarGameModal
        cls.Modal = MilionarGameModal

    def panel(self, title):
        panel = self.Modal(title=title.title, fanart='poster.jpg', plot=title.plot, genre=title.genre,
                           media_type=title.media, content=DemoContent(title, pace=0), design=designs.CLASSIC)
        panel.onInit()
        return panel

    @staticmethod
    def begin(panel):
        """Click "Začít hru", wait for the moderator's welcome, click again, wait for the first question."""
        game = panel._adapter.controller
        panel.onClick(9514)
        wait_for(lambda: game.game_state == 'welcome_screen', 'welcome of the moderator')
        panel.onClick(9514)
        wait_for(lambda: game.game_state == 'playing' and panel.getProperty('WCS.Game.ShowAnswers') == 'true',
                 'the first question')

    @staticmethod
    def labels(panel):
        return [panel.getProperty('WCS.Game.Answer' + letter) for letter in 'ABCD']

    def test_a_whole_game_through_the_panel(self):
        for title in (catalog.get('matrix'), catalog.get('chernobyl')):
            with self.subTest(title=title.id):
                panel = self.panel(title)
                self.assertEqual(panel.getProperty('WCS.Game.Poster'), 'poster.jpg')
                self.assertEqual(panel.getProperty('WCS.Game.MenuBtn1'), 'Začít hru')
                self.begin(panel)
                for round_num, item in enumerate(title.questions, 1):
                    wait_for(lambda: panel.getProperty('WCS.Game.Question') == item.text and
                             panel.getProperty('WCS.Game.ShowAnswers') == 'true', 'question {}'.format(round_num))
                    self.assertEqual(self.labels(panel), ['{}: {}'.format(letter, option)
                                                          for letter, option in zip('ABCD', item.options)])
                    panel.onClick(9500 + item.correct)
                    wait_for(lambda: panel.getProperty('WCS.Game.ShowAnswers') != 'true' and
                             panel.getProperty('WCS.Game.MenuBtn1') in ('Další otázka', 'Výsledky'), 'result buttons')
                    self.assertIn(item.explanation, panel.getProperty('WCS.Game.Text'))
                    if round_num < 9:
                        self.assertEqual(panel.getProperty('WCS.Game.MenuBtn1'), 'Další otázka')
                        panel.onClick(9514)
                self.assertEqual(panel.getProperty('WCS.Game.MenuBtn1'), 'Výsledky')
                panel.onClick(9514)
                self.assertIn('GRATULUJEME', panel.getProperty('WCS.Game.Title'))
                self.assertIn('VÍTĚZSTVÍ', panel.getProperty('WCS.Game.Text'))
                self.assertIn('Prokázali jste skvělé', panel.getProperty('WCS.Game.Text'))
                panel.onClick(9513)                          # Konec hry
                self.assertTrue(panel.closed)

    def test_lifelines_through_the_panel_and_the_ladder(self):
        title = catalog.get('lotr-fellowship')
        panel = self.panel(title)
        self.begin(panel)
        panel.onClick(9510)                                  # 50:50
        self.assertEqual(sum(1 for label in self.labels(panel) if label), 2)
        self.assertEqual(panel.getProperty('WCS.Game.Lifeline5050'), 'used')
        panel.onClick(9511)                                  # Tip
        wait_for(lambda: title.questions[0].tip in panel.getProperty('WCS.Game.Text'), 'tip')
        panel.onClick(9512)                                  # Diváci
        wait_for(lambda: 'Vyberte svou odpověď' in panel.getProperty('WCS.Game.Text'), 'poll')
        self.assertEqual(panel.getProperty('WCS.Game.LifelineAudience'), 'used')
        self.assertEqual(panel.getProperty('WCS.Game.LifelineTip'), 'used')
        self.assertTrue(panel.getProperty('WCS.Game.Ladder.1.Color'))

    def test_focus_goes_to_an_answer_that_is_left_after_fifty_fifty(self):
        from wcs.games.MillionaireMyTVAdapter import MillionaireMyTVAdapter

        class Dialog(kodi_stubs.Window):
            def __init__(self):
                super().__init__()
                self.delayed = []

            def _delayed_focus(self, control_id, delay=0.5):
                self.delayed.append(control_id)

        dialog = Dialog()
        adapter = MillionaireMyTVAdapter(dialog, '', 'Matrix', '', '', '', '', media_type='movie',
                                         content=DemoContent(catalog.get('matrix'), pace=0))
        adapter.controller.game_state = 'playing'
        adapter._on_buttons_update('A: a', 'B: b', 'C: c', 'D: d')
        adapter._on_buttons_update('', 'B: b', '', 'D: d')                   # 50:50 removed A and C
        adapter._on_buttons_update('', '', 'C: c', 'D: d')
        self.assertEqual(dialog.delayed, [9500, 9501, 9502])
        adapter._on_set_focus(3)
        self.assertEqual(dialog.focus, 9503)
        adapter._on_set_focus(0)
        self.assertEqual(dialog.focus, 9500)
        adapter.controller.game_state = 'millionaire_welcome'
        adapter._on_set_focus(0)
        self.assertEqual(dialog.focus, 9514)

    def test_a_wrong_answer_and_closing_with_back(self):
        title = catalog.get('the-simpsons')
        panel = self.panel(title)
        self.begin(panel)
        item = title.questions[0]
        panel.onClick(9500 + (item.correct + 1) % 4)
        wait_for(lambda: panel.getProperty('WCS.Game.ShowAnswers') != 'true' and
                 panel.getProperty('WCS.Game.MenuBtn1') == 'Nová hra', 'game over')
        self.assertEqual(panel.getProperty('WCS.Game.AnswerA.Color') != '', True)
        self.assertIn(item.options[item.correct], panel.getProperty('WCS.Game.Text'))

        class Back:
            def getId(self):
                return 92
        panel.onAction(Back())
        self.assertTrue(panel.closed)
        self.assertIsNone(panel._adapter)


class WiringTests(unittest.TestCase):
    def test_widget_rows_list_every_title_and_start_its_game(self):
        from wcs.skin import widgets
        for row, media in (('widget_demo_movies', 'movie'), ('widget_demo_series', 'series')):
            items = widgets.build(row, {'limit': '20'})
            expected = catalog.by_media(media)
            self.assertEqual([i.label for i in items], [t.title for t in expected])
            for item, title in zip(items, expected):
                self.assertIn('milionar_demo', item.getPath())
                self.assertIn(title.id, item.getPath())
                self.assertIn(title.poster, item.art['poster'])
                self.assertEqual(item.art['icon'], 'DefaultMovies.png' if media == 'movie' else 'DefaultTVShows.png')
        self.assertEqual(len(widgets.build('widget_demo_movies', {'limit': '3'})), 3)

    def test_the_rows_are_known_to_the_widget_manager(self):
        from wcs.skin import slots
        from wcs.library2 import clicks
        rows = slots.SECTIONS['game'][1]
        defaults = [d[0] for d in slots.SECTIONS['game'][2]]
        for row in ('widget_demo_movies', 'widget_demo_series'):
            self.assertIn(row, rows)
            self.assertIn(row, defaults)
            self.assertIn(row, slots.TITLES)
            self.assertEqual(slots.styles_of(row)[0], 'poster' if row == 'widget_demo_movies' else 'landscape')
            self.assertEqual(sorted(slots.styles_of(row)), ['landscape', 'poster'])
            self.assertEqual(clicks.options(row, 'game'), ('game', 'hero'))
            self.assertEqual(clicks.default(row), 'game')
            self.assertEqual(clicks.resolve(row, 'detail'), 'game')
            self.assertFalse(slots.has_count(row))
        for row in rows:
            self.assertIn(row, slots.TITLES)

    def test_shipped_defaults_show_the_demo_rows_in_the_game_section(self):
        with open(os.path.join(ADDON, 'resources', 'defaults', 'skin.json'), encoding='utf-8') as handle:
            game = json.load(handle)['sections']['game']['widgets']
        self.assertEqual([w['type'] for w in game], ['widget_demo_movies', 'widget_demo_series', 'widget_game_topics'])
        self.assertEqual(len({w['uid'] for w in game}), 3)
        from wcs.skin import slots
        for widget in game:
            self.assertIn(widget['type'], slots.SECTIONS['game'][1])
            self.assertEqual(widget['click'], 'game')

    def test_row_titles_exist_in_all_languages(self):
        for lang in ('cs_cz', 'sk_sk', 'en_gb'):
            path = os.path.join(ADDON, 'resources', 'language', 'resource.language.' + lang, 'strings.po')
            text = open(path, encoding='utf-8').read()
            for string_id in (32740, 32741):
                self.assertEqual(len(re.findall(r'msgctxt "#{}"'.format(string_id), text)), 1, (lang, string_id))
        text = open(os.path.join(ADDON, 'resources', 'language', 'resource.language.cs_cz', 'strings.po'),
                    encoding='utf-8').read()
        self.assertIn('Filmové výzvy (demo)', text)

    def test_launch_opens_the_title_in_the_game_panel_with_demo_content(self):
        import types
        opened = []

        class FakeModal:
            def __init__(self, **kwargs):
                opened.append(kwargs)

            def doModal(self):
                opened[-1]['modal'] = True
        module = types.ModuleType('wcs.games.DialogMilionarGame')
        module.MilionarGameModal = FakeModal
        saved = sys.modules.get('wcs.games.DialogMilionarGame')
        sys.modules['wcs.games.DialogMilionarGame'] = module
        try:
            from wcs.games.demo import launch
            launch.run({'demo': 'the-lion-king'})
            launch.run({'demo': 'does-not-exist'})
        finally:
            sys.modules['wcs.games.DialogMilionarGame'] = saved
        self.assertEqual(len(opened), 1)
        shown = opened[0]
        self.assertEqual((shown['title'], shown['media_type'], shown['modal']), ('Lví král', 'movie', True))
        self.assertTrue(shown['fanart'].endswith(catalog.get('the-lion-king').poster))
        self.assertEqual(shown['content'].name, 'demo')

    def test_main_routes_the_demo_action(self):
        source = open(os.path.join(ADDON, 'main.py'), encoding='utf-8').read()
        self.assertRegex(source, r"_action == 'milionar_demo':\s+# .*\n\s+from wcs.games.demo import launch\n\s+launch.run\(_params\)")

    def test_game_texts_never_mention_ai_in_the_demo_screens(self):
        for title in catalog.titles():
            game, screen = make_game(title)
            game.start_specific_game()
            game.handle_game_choice(1)
            for text in screen.texts:
                self.assertNotIn('AI vytváří', text)


class HomeSectionTests(unittest.TestCase):
    """The cinematic Milionář section of Home: hero data of the cards, the tab buttons, the random game."""

    def setUp(self):
        import xbmc
        self.xbmc, self.original = xbmc, xbmc.getInfoLabel
        xbmc.getInfoLabel = lambda label: {'Skin.String(mil.game.1.type)': 'widget_demo_movies',
                                           'Skin.String(mil.game.2.type)': 'widget_demo_series'}.get(label, '')

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

    def test_every_card_carries_the_hero_of_the_section(self):
        from wcs.skin import hero, widgets
        from wcs.skin.hero_contract import BASES
        for row, kicker in (('widget_demo_movies', 'FILMOVÁ VÝZVA'), ('widget_demo_series', 'SERIÁLOVÁ VÝZVA')):
            for item, title in zip(widgets.build(row, {'limit': '20'}),
                                   catalog.by_media('movie' if row == 'widget_demo_movies' else 'series')):
                prop = item.getProperty
                self.assertEqual(prop('milionar.hero.title'), title.title)
                spaced = re.sub(r'[\s\u200a\u00a0]', '', prop('milionar.hero.kicker'))          # the kicker is letter-spaced
                self.assertEqual(spaced, re.sub(r'\s', '', '{}·{}'.format(kicker, title.year)))
                self.assertIn(title.fanart, prop('milionar.hero.fanart'))
                # the game hero shows the title's plot like the library hero (widgets._challenge)
                self.assertEqual(prop('milionar.hero.plot'), hero.plot(title.plot))
                self.assertEqual(prop('milionar.hero.directs'), 'tab_movies,tab_series,random_game,ai_costs')
                self.assertEqual(prop('milionar.hero.do.tab_movies'), 'SetFocus({})'.format(BASES['game'] + 100))
                self.assertEqual(prop('milionar.hero.do.tab_series'), 'SetFocus({})'.format(BASES['game'] + 200))
                self.assertIn('action=milionar_demo&demo=random', prop('milionar.hero.do.random_game'))
                self.assertEqual(prop('milionar.hero.dl.random_game'), '32749')

    def test_a_tab_whose_row_is_not_in_the_section_is_left_out(self):
        from wcs.skin import widgets
        self.xbmc.getInfoLabel = lambda label: 'widget_demo_series' if label == 'Skin.String(mil.game.1.type)' else ''
        actions = [b[0] for b in widgets._game_buttons({})]
        self.assertEqual(actions, ['tab_series', 'random_game', 'ai_costs'])     # the AI costs button is always last

    def test_the_hero_buttons_of_the_section_are_known_to_the_widget_manager(self):
        from wcs.skin import context
        for row in context.GAME_ROWS:
            self.assertEqual(context.catalog(row)[:len(context.GAME_HERO)], context.GAME_HERO)
        for action in context.GAME_HERO:
            self.assertIn(action, context.LABELS)

    def test_random_game_picks_a_title_of_the_catalog(self):
        import random
        import types
        opened = []

        class FakeModal:
            def __init__(self, **kwargs):
                opened.append(kwargs)

            def doModal(self):
                pass
        module = types.ModuleType('wcs.games.DialogMilionarGame')
        module.MilionarGameModal = FakeModal
        saved = sys.modules.get('wcs.games.DialogMilionarGame')
        sys.modules['wcs.games.DialogMilionarGame'] = module
        try:
            from wcs.games.demo import launch
            for _ in range(15):
                launch.run({'demo': 'random'})
        finally:
            sys.modules['wcs.games.DialogMilionarGame'] = saved
        titles = {t.title for t in catalog.titles()}
        self.assertEqual(len(opened), 15)
        self.assertTrue(all(o['title'] in titles and o['content'].name == 'demo' for o in opened))
        self.assertGreater(len({o['title'] for o in opened}), 1)
        self.assertTrue(all(o['banner_fanart'] and o['year'] for o in opened))


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