# -*- coding: utf-8 -*-
"""Offline tests of the modern card of the Millionaire game: the view model of game_view.py, the contract between
the generated XML and the properties, the setting that picks the design, and whole games played through the modern
card's controls (intents), lifelines, the exit confirmation.

Run: python3 tests/test_millionaire_modern.py
"""
import os
import re
import sys
import time
import unittest
import xml.etree.ElementTree as ET

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

kodi_stubs.install()
ADDON = kodi_stubs.ADDON_DIR
sys.path.insert(0, os.path.join(ADDON, 'scripts', 'game_dialog'))

import test_millionaire_demo as base                       # noqa: E402
from wcs.games import designs, game_settings, game_view    # noqa: E402
from wcs.games.content.demo import DemoContent             # noqa: E402
from wcs.games.demo import catalog                         # noqa: E402

wait_for = base.wait_for
# every property of the modern card that is not the view's: static facts, the adapter's flags, the window's own
STATIC_PROPERTIES = {'Name', 'Year', 'Genre', 'Poster', 'Fanart', 'Text', 'ButtonsReady', 'Exit', 'Settings', 'SetTitle',
                     'SetValue', 'SetDesc', 'SetChoices', 'Mode', 'StoryAvailable'}


def setUpModule():
    base.setUpModule()


def tearDownModule():
    base.tearDownModule()


class ModernPanelCase(unittest.TestCase):
    """Helpers: a modern card with a demo game, driven by control ids as the player would."""

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

    def panel(self, title_id='matrix', content=None, design=designs.MODERN):
        title = catalog.get(title_id)
        panel = self.Modal(title=title.title, fanart='poster.jpg', banner_fanart='backdrop.jpg', plot=title.plot,
                           genre=title.genre, media_type=title.media, year=title.year,
                           content=content or DemoContent(title, pace=0), design=design)
        panel.onInit()
        self.title = title
        return panel

    @staticmethod
    def screen(panel):
        return panel.getProperty('WCS.Game.Screen')

    def until(self, panel, screen, what=None):
        wait_for(lambda: self.screen(panel) == screen and panel.getProperty('WCS.Game.ButtonsReady') == 'true',
                 what or 'screen ' + screen)

    def to_question(self, panel):
        self.until(panel, 'title')
        panel.onClick(9601)
        wait_for(lambda: self.screen(panel) == 'intro' and panel.getProperty('WCS.Game.Moderator'), 'intro')
        self.until(panel, 'intro')
        panel.onClick(9621)
        wait_for(lambda: self.screen(panel) == 'question' and panel.getProperty('WCS.Game.OptA'), 'question')

    def answer(self, panel, index, then='feedback'):
        panel.onClick(9500 + index)
        self.until(panel, then, 'the result of the answer')


class ViewModelTests(unittest.TestCase):
    def test_screens_follow_the_state_of_the_game_in_order(self):
        game, screen = base.make_game(catalog.get('friends'))
        seen = []
        game.start_specific_game()
        seen.append(game_view.view(game)['Screen'])
        game.handle_game_choice(1)
        seen.append(game_view.view(game)['Screen'])
        game.handle_game_choice(1)
        game.handle_game_choice(0)
        wait_for(lambda: game.game_state == 'welcome_screen', 'welcome')
        seen.append(game_view.view(game)['Screen'])
        game.handle_game_choice(0)
        wait_for(lambda: game.game_state == 'playing', 'question')
        seen.append(game_view.view(game)['Screen'])
        base.answer(game, screen, game.current_question['correct'])
        seen.append(game_view.view(game)['Screen'])
        self.assertEqual(seen, ['title', 'rules', 'intro', 'question', 'feedback'])

    def test_a_question_screen_carries_everything_a_design_shows(self):
        title = catalog.get('stranger-things')
        game, _screen = base.make_game(title)
        base.start_game(game)
        view = game_view.view(game)
        item = title.questions[0]
        self.assertEqual(view['QuestionText'], item.text)
        self.assertEqual([view['Opt' + letter] for letter in 'ABCD'], list(item.options))
        self.assertEqual((view['RoundLabel'], view['TierLabel'], view['TierKey']), ('OTÁZKA 1 / 9', 'SNADNÁ', 'easy'))
        self.assertEqual((view['Money'], view['Earned']), ('1 000 Kč', '0 Kč'))
        self.assertEqual(view['Ladder.1.State'], 'current')
        self.assertTrue(all(view['Ladder.{}.State'.format(n)] == 'todo' for n in range(2, 10)))
        self.assertEqual(view['TipLabel'], 'Tip od tvůrce')
        self.assertEqual(view['Kicker'], 'SERIÁLOVÁ VÝZVA')
        self.assertEqual(view['Tagline'], title.tagline)
        self.assertEqual(view['HelpKind'], '')

    def test_tiers_and_ladder_while_the_game_goes_on(self):
        game, screen = base.make_game(catalog.get('matrix'))
        base.start_game(game)
        for round_num in range(1, 8):
            view = game_view.view(game)
            self.assertEqual(view['TierKey'], 'easy' if round_num <= 3 else 'medium' if round_num <= 6 else 'hard')
            self.assertEqual(view['Ladder.{}.State'.format(round_num)], 'current')
            self.assertEqual(view['CorrectCount'], str(round_num - 1))
            base.answer(game, screen, game.current_question['correct'])
            feedback = game_view.view(game)
            self.assertEqual((feedback['Screen'], feedback['Earned']), ('feedback', game_view.money(round_num)))
            self.assertEqual(feedback['Ladder.{}.State'.format(round_num)], 'done')
            # the answer screen lights the next amount (its ladder is headed "Další otázka za")
            self.assertEqual(feedback['Ladder.{}.State'.format(round_num + 1)], 'current')
            self.assertEqual(feedback['NextMoney'], game_view.money(round_num + 1))
            game.handle_game_choice(0)
            base.wait_for(lambda: game.game_state == 'playing' and game.current_round == round_num + 1, 'next')

    def test_a_long_moderator_text_switches_the_card_to_the_small_font(self):
        game, screen = base.make_game(catalog.get('matrix'))
        base.start_game(game)
        base.answer(game, screen, game.current_question['correct'])
        self.assertEqual(game_view.view(game)['ModeratorSmall'], '')          # the demo's short words fit
        game.feedback_data = {'result_announcement': 'Slovo ' * 60, 'transition_text': 'Dál ' * 25}
        self.assertEqual(game_view.view(game)['ModeratorSmall'], 'true')
        self.assertFalse(game_view.moderator_small('a' * 140, 'b' * 60))     # 2 + 1 lines
        self.assertTrue(game_view.moderator_small('a ' * 200, 'b'))           # 6 lines + 1

    def test_game_over_shows_both_answers_and_what_was_reached(self):
        title = catalog.get('titanic')
        game, screen = base.make_game(title)
        base.start_game(game)
        for _ in range(2):
            base.answer(game, screen, game.current_question['correct'])
            game.handle_game_choice(0)
            base.wait_for(lambda: game.game_state == 'playing', 'next')
        item = title.questions[2]
        wrong = (item.correct + 2) % 4
        base.answer(game, screen, wrong)
        view = game_view.view(game)
        self.assertEqual(view['Screen'], 'gameover')
        self.assertEqual((view['YourLetter'], view['YourText']), ('ABCD'[wrong], item.options[wrong]))
        self.assertEqual((view['RightLetter'], view['RightText']), ('ABCD'[item.correct], item.options[item.correct]))
        self.assertEqual((view['CorrectLabel'], view['Earned']), ('2 z 9', '2 000 Kč'))
        self.assertEqual(view['Ladder.3.State'], 'failed')
        self.assertEqual(view['Explanation'], item.explanation)

    def test_win_and_results_summarise_the_game(self):
        game, screen = base.make_game(catalog.get('the-godfather'))
        base.start_game(game)
        game.use_5050_lifeline()
        base.play_to_the_end(game, screen)
        win = game_view.view(game)
        self.assertEqual((win['Screen'], win['Earned'], win['CorrectCount']), ('win', '1 000 000 Kč', '9'))
        game.handle_game_choice(0)
        results = game_view.view(game)
        self.assertEqual(results['Screen'], 'results')
        self.assertEqual((results['TierEasy'], results['TierMedium'], results['TierHard']), ('3/3', '3/3', '3/3'))
        self.assertEqual((results['Score'], results['Life5050Used'], results['LifeTipUsed'], results['LivesUsed']),
                         ('9 / 9', 'true', '', '1'))

    def test_every_key_is_always_present_and_texts_do_not_leak_between_screens(self):
        game, screen = base.make_game(catalog.get('sherlock'))
        keys = set(game_view.keys())
        game.start_specific_game()
        self.assertEqual(set(game_view.view(game)), keys)
        base.start_game(game)
        self.assertEqual(set(game_view.view(game)), keys)
        base.answer(game, screen, game.current_question['correct'])
        base.play_to_the_end  # (not needed further)
        view = game_view.view(game)
        self.assertEqual(set(view), keys)
        self.assertEqual((view['OptA'], view['StateA'], view['HelpKind']), (view['OptA'], '', ''))
        game.handle_game_choice(0)
        base.wait_for(lambda: game.game_state == 'playing', 'next')
        self.assertEqual(game_view.view(game)['Result'], '')           # the result of the last round is gone

    def test_generic_lines_when_the_content_has_none(self):
        class Plain:
            tagline = win_line = ''
        game, _screen = base.make_game(catalog.get('matrix'), content=DemoContent(catalog.get('matrix'), pace=0))
        game.content = Plain()
        game.media_type, game.title = 'series', 'X'
        view = game_view.view(game)
        self.assertEqual(view['Tagline'], 'Jak dobře znáte tento seriál?')
        self.assertEqual(view['WinLine'], 'Znáte X do posledního detailu.')


class ContractTests(unittest.TestCase):
    """The generated XML and the properties the game publishes must fit each other."""

    @classmethod
    def setUpClass(cls):
        cls.path = os.path.join(ADDON, 'resources', 'skins', 'Default', '1080i', 'games', 'DialogMilionarGameModern.xml')
        cls.text = open(cls.path, encoding='utf-8').read()
        cls.root = ET.fromstring(cls.text)

    def test_the_xml_is_the_generated_one(self):
        import gen_modern_game
        self.assertEqual(gen_modern_game.build(), self.text, 'run scripts/game_dialog/gen_modern_game.py')

    def test_control_ids_are_unique_and_cover_every_control_of_the_design(self):
        ids = [int(c.get('id')) for c in self.root.iter('control') if c.get('id')]
        self.assertEqual(len(ids), len(set(ids)))
        design = designs.MODERN
        for control in design.clicks:
            self.assertIn(control, ids, 'a click maps control {} but the XML has none'.format(control))
        for screen, control in design.primary.items():
            self.assertIn(control, ids, screen)
        self.assertIn(int(self.root.find('defaultcontrol').text), ids)

    def test_every_property_the_xml_reads_is_published(self):
        used = set(re.findall(r'WCS\.Game\.([A-Za-z0-9._]+)\)', self.text))
        known = set(game_view.keys()) | STATIC_PROPERTIES | {name[len('WCS.Game.'):] for name in game_settings.properties()}
        known |= {name[len('WCS.Game.'):] for name in game_settings.properties(rows=game_settings.rows_for('story'))}
        self.assertEqual(sorted(used - known), [], 'the XML reads properties nobody sets')

    def test_every_screen_has_a_group_and_a_first_control(self):
        screens = set(game_view.SCREENS.values()) | {'results'}
        shown = set(re.findall(r'String\.IsEqual\(Window\.Property\(WCS\.Game\.Screen\),(\w+)\)', self.text))
        self.assertEqual(screens - shown, set())
        self.assertEqual(shown - screens, set())
        self.assertEqual(screens - set(designs.MODERN.primary), set())

    def test_navigation_targets_exist(self):
        ids = {c.get('id') for c in self.root.iter('control') if c.get('id')}
        for control in self.root.iter('control'):
            for direction in ('onup', 'ondown', 'onleft', 'onright'):
                node = control.find(direction)
                if node is not None and node.text != 'noop':
                    self.assertIn(node.text, ids, 'control {} -> {} {}'.format(control.get('id'), direction, node.text))

    def test_no_label_is_a_bare_number_and_textures_exist(self):
        for node in self.root.iter('label'):
            self.assertFalse((node.text or '').strip().isdigit(), 'Kodi would read "{}" as a string id'.format(node.text))
        media = os.path.join(ADDON, 'resources', 'media')
        for node in self.root.iter('texture'):
            for value in [node.text or ''] + [node.get('diffuse') or '', node.get('fallback') or '']:
                if value.startswith('special://home/addons/plugin.video.milionar/resources/media/'):
                    name = value.split('/resources/media/', 1)[1]
                    self.assertTrue(os.path.exists(os.path.join(media, name)), name)


class DesignSettingTests(unittest.TestCase):
    def test_the_setting_picks_the_design(self):
        import xbmcaddon
        original = xbmcaddon.Addon.getSetting
        try:
            for value, expected in (('0', designs.CLASSIC), ('1', designs.MODERN), ('', designs.MODERN),
                                    ('7', designs.MODERN)):
                xbmcaddon.Addon.getSetting = lambda self, key, value=value: value if key == designs.SETTING else ''
                self.assertIs(designs.current(), expected, value)
        finally:
            xbmcaddon.Addon.getSetting = original

    def test_the_window_loads_the_xml_of_the_chosen_design(self):
        from wcs.games.DialogMilionarGame import MilionarGameModal
        for design in (designs.CLASSIC, designs.MODERN):
            panel = MilionarGameModal(title='X', design=design, content=DemoContent(catalog.get('matrix'), pace=0))
            self.assertIs(panel._design, design)
        for design in (designs.CLASSIC, designs.MODERN):
            self.assertTrue(os.path.exists(os.path.join(ADDON, 'resources', 'skins', 'Default', '1080i', design.xml)))

    def test_the_setting_and_its_strings_exist(self):
        settings = open(os.path.join(ADDON, 'resources', 'settings.xml'), encoding='utf-8').read()
        match = re.search(r'<setting[^>]*id="milionar_game_design"[^>]*>', settings)
        self.assertIsNotNone(match)
        self.assertIn('type="select"', match.group(0))
        self.assertIn('lvalues="32745|32746"', match.group(0))
        category = settings[settings.index('<category label="32100">'):]
        self.assertLess(category.index('milionar_game_design'), category.index('millionaire_difficulty'),
                        'the look comes first in the Milionář section')
        for lang in ('cs_cz', 'sk_sk', 'en_gb'):
            po = open(os.path.join(ADDON, 'resources', 'language', 'resource.language.' + lang, 'strings.po'),
                      encoding='utf-8').read()
            for string_id in range(32742, 32747):
                self.assertEqual(len(re.findall(r'msgctxt "#{}"'.format(string_id), po)), 1, (lang, string_id))
        cs = open(os.path.join(ADDON, 'resources', 'language', 'resource.language.cs_cz', 'strings.po'),
                  encoding='utf-8').read()
        self.assertIn('Klasická karta', cs)
        self.assertIn('Moderní karta', cs)


class ModernGameTests(ModernPanelCase):
    def test_the_title_screen_and_the_static_facts(self):
        panel = self.panel('matrix')
        self.until(panel, 'title')
        self.assertEqual((panel.getProperty('WCS.Game.Name'), panel.getProperty('WCS.Game.Year'),
                          panel.getProperty('WCS.Game.Genre')), ('Matrix', '1999', 'Akční, sci-fi'))
        self.assertEqual((panel.getProperty('WCS.Game.Poster'), panel.getProperty('WCS.Game.Fanart')),
                         ('poster.jpg', 'backdrop.jpg'))
        wait_for(lambda: panel.focus == 9601, 'focus on "Začít hru"')

    def test_rules_and_back_to_the_title(self):
        panel = self.panel()
        self.until(panel, 'title')
        panel.onClick(9602)
        self.until(panel, 'rules')
        wait_for(lambda: panel.focus == 9611, 'focus on the rules screen')
        panel.onClick(9612)
        self.until(panel, 'title')

    def test_a_whole_game_through_the_modern_controls(self):
        for title_id in ('matrix', 'chernobyl'):
            with self.subTest(title=title_id):
                panel = self.panel(title_id)
                self.to_question(panel)
                for round_num, item in enumerate(self.title.questions, 1):
                    wait_for(lambda: panel.getProperty('WCS.Game.QuestionText') == item.text, 'question {}'.format(round_num))
                    self.assertEqual(panel.getProperty('WCS.Game.RoundLabel'), 'OTÁZKA {} / 9'.format(round_num))
                    self.assertEqual([panel.getProperty('WCS.Game.Opt' + l) for l in 'ABCD'], list(item.options))
                    self.answer(panel, item.correct, 'feedback' if round_num < 9 else 'win')
                    if round_num < 9:
                        self.assertEqual(panel.getProperty('WCS.Game.Explanation'), item.explanation)
                        wait_for(lambda: panel.focus == 9641, 'focus on "Další otázka"')
                        panel.onClick(9641)
                self.assertEqual(panel.getProperty('WCS.Game.Earned'), '1 000 000 Kč')
                panel.onClick(9661)
                self.until(panel, 'results')
                self.assertEqual(panel.getProperty('WCS.Game.TierHard'), '3/3')
                panel.onClick(9672)                           # Hrát znovu
                wait_for(lambda: self.screen(panel) == 'intro', 'a new game starts at the intro')

    def test_a_wrong_answer_then_play_again_or_leave(self):
        panel = self.panel('titanic')
        self.to_question(panel)
        item = self.title.questions[0]
        self.answer(panel, (item.correct + 1) % 4, 'gameover')
        self.assertEqual(panel.getProperty('WCS.Game.CorrectLabel'), '0 z 9')
        wait_for(lambda: panel.focus == 9651, 'focus on "Hrát znovu"')
        panel.onClick(9651)
        wait_for(lambda: self.screen(panel) == 'intro', 'intro of the new game')
        panel = self.panel('titanic')
        self.to_question(panel)
        self.answer(panel, (item.correct + 1) % 4, 'gameover')
        panel.onClick(9652)                                   # Vybrat jiný titul
        self.assertTrue(panel.closed)

    def test_home_leaves_and_focuses_the_main_menu(self):
        panel = self.panel()
        self.to_question(panel)
        self.answer(panel, (self.title.questions[0].correct + 1) % 4, 'gameover')
        kodi_stubs.BUILTINS.clear()
        panel.onClick(9653)
        self.assertTrue(panel.closed)
        self.assertIn('SetFocus(9000)', kodi_stubs.BUILTINS)

    def test_lifelines_and_the_cards_they_fill(self):
        panel = self.panel('lotr-fellowship')
        self.to_question(panel)
        panel.onClick(9631)
        self.assertEqual(panel.getProperty('WCS.Game.Life5050Used'), 'true')
        removed = [l for l in 'ABCD' if panel.getProperty('WCS.Game.State' + l) == 'removed']
        self.assertEqual(len(removed), 2)
        panel.onClick(9632)
        wait_for(lambda: panel.getProperty('WCS.Game.TipState') == 'ready', 'the tip')
        self.assertEqual((panel.getProperty('WCS.Game.HelpKind'), panel.getProperty('WCS.Game.TipText')),
                         ('tip', self.title.questions[0].tip))
        panel.onClick(9633)
        wait_for(lambda: panel.getProperty('WCS.Game.PollState') == 'final', 'the poll')
        self.assertEqual(panel.getProperty('WCS.Game.HelpKind'), 'poll')
        poll = [int(panel.getProperty('WCS.Game.Poll' + l)) for l in 'ABCD']
        self.assertEqual(sum(poll), 100)
        self.assertTrue(all(poll['ABCD'.index(l)] == 0 for l in removed))
        self.assertEqual(panel.getProperty('WCS.Game.LivesUsed'), '3')
        item = self.title.questions[0]
        self.answer(panel, item.correct)
        panel.onClick(9641)
        wait_for(lambda: panel.getProperty('WCS.Game.Round') == '2', 'question 2')
        self.assertEqual((panel.getProperty('WCS.Game.HelpKind'), panel.getProperty('WCS.Game.StateA')), ('', ''))
        self.assertEqual(panel.getProperty('WCS.Game.Life5050Used'), 'true', 'a used lifeline stays used')

    def test_the_countdown_is_published(self):
        panel = self.panel()
        self.to_question(panel)
        seen = []
        original = panel.setProperty
        panel.setProperty = lambda key, value: (seen.append(value) if key == 'WCS.Game.Countdown' else None,
                                                original(key, value))
        panel.onClick(9500 + (self.title.questions[0].correct + 1) % 4)
        self.until(panel, 'gameover')
        counted = [value for value in seen if value]
        self.assertEqual(counted[:3], ['3', '2', '1'], 'the seconds reach the card one by one')
        self.assertEqual(panel.getProperty('WCS.Game.Countdown'), '')

    def test_back_returns_to_title_then_closes(self):
        class Back:
            def getId(self):
                return 92
        panel = self.panel()
        self.to_question(panel)
        retired = panel._adapter.controller
        panel.onAction(Back())
        self.assertEqual(panel.getProperty('WCS.Game.Exit'), 'true')
        panel.onClick(9691)
        self.assertIs(panel._adapter.controller, retired)
        self.assertEqual(self.screen(panel), 'question')
        self.assertEqual(panel.getProperty('WCS.Game.Exit'), '')
        panel.onAction(Back())
        panel.onClick(9692)
        self.until(panel, 'title')
        self.assertFalse(panel.closed)
        retired.show_moderator_welcome_screen()
        self.assertEqual(self.screen(panel), 'title', 'retired workers cannot replace the menu')
        panel.onAction(Back())
        self.assertTrue(panel.closed)

    def test_intro_back_button_returns_to_title(self):
        panel = self.panel()
        self.until(panel, 'title')
        panel.onClick(9601)
        self.until(panel, 'intro')
        panel.onClick(9622)
        self.until(panel, 'title')
        self.assertFalse(panel.closed)

    def test_a_failing_moderator_shows_the_error_screen_and_retry_works(self):
        class Broken(DemoContent):
            fail = True

            def welcome(self):
                if Broken.fail:
                    raise RuntimeError('moderator is out')
                return super().welcome()
        panel = self.panel('matrix', content=Broken(catalog.get('matrix'), pace=0))
        self.until(panel, 'title')
        panel.onClick(9601)
        self.until(panel, 'error')
        self.assertIn('moderator is out', panel.getProperty('WCS.Game.Text'))
        Broken.fail = False
        panel.onClick(9681)
        wait_for(lambda: self.screen(panel) == 'intro' and panel.getProperty('WCS.Game.Moderator'), 'intro after retry')

    def test_the_classic_card_is_not_touched_by_the_modern_properties(self):
        from wcs.games.DialogMilionarGame import MilionarGameModal
        panel = MilionarGameModal(title='Matrix', fanart='p.jpg', media_type='movie', design=designs.CLASSIC,
                                  content=DemoContent(catalog.get('matrix'), pace=0))
        panel.onInit()
        wait_for(lambda: panel.getProperty('WCS.Game.MenuBtn1') == 'Začít hru', 'classic title')
        self.assertEqual(panel.getProperty('WCS.Game.Screen'), '', 'the classic card never gets the modern properties')


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