"""Offline coverage of every authored Story demo through the production source and modal."""
import json
import os
import shutil
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock

sys.path.insert(0, os.path.dirname(__file__))
import kodi_stubs
kodi_stubs.install()

import xbmcaddon
from test_story_game import wait
from wcs.games import designs, millionaire_tts
from wcs.games.content.demo import DemoContent
from wcs.games.demo import catalog
from wcs.games.DialogMilionarGame import MilionarGameModal
from wcs.games.millionaire.subject import Subject
from wcs.games.story import content
from wcs.games.story.ai import AiSource
from wcs.games.story.engine import Engine
from wcs.games.story.view import view

EPISODES = {'friends', 'house-md', 'sherlock', 'stranger-things', 'the-big-bang-theory',
            'the-mandalorian', 'the-office-us', 'the-simpsons'}


def subject(title):
    return Subject(title=title.title, year=title.year, media_type=title.media, tmdb_id=str(title.tmdb_id))


class CatalogTests(unittest.TestCase):
    def test_every_existing_title_has_three_packs_and_only_approved_episode_scopes(self):
        root = Path(content.DEMO_DIR)
        titles = catalog.titles()
        self.assertEqual(len(titles), 29)
        self.assertEqual({p.name for p in root.iterdir() if p.is_dir()}, {t.id for t in titles})
        self.assertEqual(len(list(root.glob('*/*.json'))), 87)
        scopes = json.loads((root / 'scopes.json').read_text())
        self.assertEqual(set(scopes), EPISODES)
        for title in titles:
            with self.subTest(title=title.id):
                self.assertTrue(content.demo_available(title.id))
                self.assertEqual({p.stem for p in (root / title.id).glob('*.json')}, set(content.COUNTS))
                scoped = content.DemoSource(title.id).scoped_subject(subject(title))
                self.assertEqual(scoped.tmdb_id, str(title.tmdb_id))
                self.assertEqual((scoped.season_number, scoped.episode_number),
                                 (1, 1) if title.id in EPISODES else (None, None))
                self.assertEqual(bool(scoped.episode_title), title.id in EPISODES)

    def test_all_packs_validate_identity_replay_and_cover_the_ending(self):
        for title in catalog.titles():
            source = content.DemoSource(title.id)
            endings, openings, last_questions = set(), set(), set()
            for difficulty, count in content.COUNTS.items():
                with self.subTest(title=title.id, difficulty=difficulty):
                    pack = source.prepare(subject(title), difficulty)
                    self.assertEqual(pack, source.prepare(subject(title), difficulty))
                    self.assertEqual(len(pack.scenes), count)
                    self.assertEqual([sum(s.chapter == n for s in pack.scenes) for n in range(1, 6)],
                                     [count // 5] * 5)
                    self.assertGreaterEqual(len({s.correct for s in pack.scenes}), 3)
                    with self.assertRaises(content.ContentError):
                        source.prepare(subject(title)._replace(title='A different work'), difficulty)
                    endings.add(pack.ending)
                    openings.add(pack.scenes[0].setup)
                    last_questions.add(pack.scenes[-1].question)
            self.assertEqual(len(endings), 1)
            self.assertEqual(len(openings), 1)
            if title.id != 'matrix':  # Preserve the existing independently authored Matrix variants.
                self.assertEqual(len(last_questions), 1)

    def test_every_pack_wins_with_two_errors_and_loses_on_the_third_final_error(self):
        for title in catalog.titles():
            for difficulty, count in content.COUNTS.items():
                pack = content.DemoSource(title.id).prepare(subject(title), difficulty)
                for errors in ((), (1, count), (1, 2, count)):
                    with self.subTest(title=title.id, difficulty=difficulty, errors=errors):
                        engine = Engine(pack)
                        for number in range(1, count + 1):
                            hidden = view(engine, 'question', difficulty, str)
                            for key in ('Ending', 'Reveal', 'RightLetter', 'RightText'):
                                self.assertEqual(hidden[key], '')
                            right = engine.scene.correct
                            self.assertTrue(engine.answer((right + 1) % 4 if number in errors else right))
                            self.assertEqual(engine.lives, 3 - sum(n <= number for n in errors))
                            if number < count:
                                self.assertTrue(engine.advance())
                        lost = len(errors) == 3
                        self.assertEqual(engine.state, 'lost' if lost else 'won')
                        self.assertEqual(engine.correct, count - len(errors))
                        self.assertFalse(engine.advance())
                        self.assertEqual(view(engine, 'gameover' if lost else 'win', difficulty, str)['Ending'],
                                         '' if lost else pack.ending)

    def test_missing_pack_unknown_or_unsafe_ids_do_not_enable_a_source(self):
        for value in ('unknown-title', '../matrix', '/matrix', '', None, 12):
            self.assertFalse(content.demo_available(value))
        with tempfile.TemporaryDirectory() as folder:
            root = Path(folder)
            (root / 'matrix').mkdir()
            shutil.copy(Path(content.DEMO_DIR) / 'scopes.json', root / 'scopes.json')
            for difficulty in ('easy', 'medium'):
                shutil.copy(Path(content.DEMO_DIR) / 'matrix' / (difficulty + '.json'), root / 'matrix')
            with mock.patch.object(content, 'DEMO_DIR', folder):
                self.assertFalse(content.demo_available('matrix'))
                with self.assertRaises(content.Unavailable):
                    content.DemoSource('matrix').prepare(subject(catalog.get('matrix')), 'easy')

    def test_explicit_caller_episode_or_season_is_never_replaced(self):
        source = content.DemoSource('friends')
        for scope in ({'season_number': 2}, {'episode_number': 2}, {'episode_title': 'Another episode'},
                      {'season_number': 1, 'episode_number': 2, 'episode_title': 'Another episode'}):
            selected = subject(catalog.get('friends'))._replace(**scope)
            self.assertIs(source.scoped_subject(selected), selected)
            with self.assertRaises(content.ContentError):
                source.prepare(selected, 'medium')


class ModalTests(unittest.TestCase):
    def setUp(self):
        self.settings = {'story_difficulty': '1', 'game_last_mode': 'story',
                         **{'milionar_tts_' + key: 'true' for key in (
                             'welcome', 'intro', 'question', 'options', 'result', 'transition', 'explanation')}}
        settings = self.settings

        class Addon:
            getSetting = lambda self, key: settings.get(key, '')
            setSetting = lambda self, key, value: settings.__setitem__(key, value)
            getAddonInfo = lambda self, key: kodi_stubs.ADDON_DIR if key == 'path' else 'plugin.video.milionar'
            getLocalizedString = lambda self, key: str(key)
        self.addCleanup(mock.patch.stopall)
        mock.patch.object(xbmcaddon, 'Addon', lambda *args: Addon()).start()
        self.ai = mock.patch.object(AiSource, 'prepare', side_effect=AssertionError('Demo called AI')).start()
        self.tts = mock.patch.object(millionaire_tts, 'play_tts', side_effect=AssertionError('Demo called TTS')).start()
        self.network = mock.patch('socket.socket.connect', side_effect=AssertionError('Demo used network')).start()
        self.panels = []

    def tearDown(self):
        for panel in self.panels:
            panel.close()
        self.ai.assert_not_called()
        self.tts.assert_not_called()
        self.network.assert_not_called()

    def panel(self, title, mode='millionaire', demo_id=None, selected=None, source=None):
        selected = selected or subject(title)
        panel = MilionarGameModal(title=selected.title, year=selected.year, media_type=selected.media_type,
                                 subject=selected, content=DemoContent(title, pace=0), design=designs.MODERN,
                                 mode=mode, demo_id=title.id if demo_id is None else demo_id, story_source=source)
        panel._delayed_focus = lambda control, delay=0: panel.setFocusId(control)
        self.panels.append(panel)
        panel.onInit()
        return panel

    def test_all_games_share_the_selector_play_replay_and_restore_millionaire_scope(self):
        for title in catalog.titles():
            for level, count in enumerate((10, 15, 20)):
                with self.subTest(title=title.id, count=count):
                    self.settings['story_difficulty'] = str(level)
                    panel = self.panel(title)
                    original_subject, original_content = panel.subject, panel.content
                    panel.onClick(9606)
                    self.assertEqual(panel.mode, 'story')
                    self.assertIs(panel.subject, original_subject)
                    self.assertEqual('S01E01' in panel.getProperty('WCS.Story.SourceNote'), title.id in EPISODES)
                    self.assertEqual(panel.getProperty('WCS.Game.Name'), title.title)
                    panel.onClick(9601)
                    wait(lambda: panel._adapter.screen() == 'question')
                    pack = panel._adapter.engine.pack
                    for number in range(1, count + 1):
                        engine = panel._adapter.engine
                        answer = (engine.scene.correct + 1) % 4 if number in (1, count) else engine.scene.correct
                        panel.onClick(9790 + answer)
                        if number < count:
                            panel.onClick(9794)
                    self.assertEqual(panel._adapter.screen(), 'win')
                    self.assertEqual(panel.getProperty('WCS.Story.Lives'), '1')
                    self.assertEqual(panel.getProperty('WCS.Story.Ending'), pack.ending)
                    panel.onClick(9794)
                    wait(lambda: panel._adapter.screen() == 'question')
                    self.assertEqual(panel._adapter.engine.pack, pack)
                    self.assertEqual(panel._adapter.engine.lives, 3)
                    panel.onClick(9785)
                    panel.onClick(9692)
                    panel.onClick(9605)
                    self.assertEqual(panel.mode, 'millionaire')
                    self.assertIs(panel.content, original_content)
                    controller = panel._adapter.controller
                    self.assertIs(controller.content, original_content)
                    self.assertEqual((controller.season_number, controller.episode_number, controller.episode_title),
                                     (None, None, ''))
                    self.assertEqual(panel.getProperty('WCS.Story.SourceNote'), '')
                    panel.close()

    def test_unavailable_demo_keeps_saved_preference_and_cannot_select_story(self):
        panel = self.panel(catalog.get('matrix'), mode=None, demo_id='unknown-title')
        self.assertEqual(panel.mode, 'millionaire')
        self.assertFalse(panel.story_available)
        panel.onClick(9606)
        self.assertEqual(panel.mode, 'millionaire')
        self.assertEqual(self.settings['game_last_mode'], 'story')

    def test_partial_caller_scope_displays_an_honest_error_without_ai_or_replacement(self):
        title = catalog.get('friends')
        selected = subject(title)._replace(season_number=2)
        panel = self.panel(title, mode='story', selected=selected)
        self.assertIs(panel._adapter.subject, selected)
        self.assertNotIn('S01E01', panel.getProperty('WCS.Story.SourceNote'))
        panel.onClick(9601)
        wait(lambda: panel._adapter.screen() == 'error')
        self.assertEqual(panel.getProperty('WCS.Story.Lives'), '3')


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