# -*- coding: utf-8 -*-
"""Regression test: the live AI path of the Millionaire game still works through the content-source seam.

The AI client is replaced by a fake that answers like the real chat (JSON of welcome / question / feedback), so no
AI call is made. Kept apart from test_millionaire_demo.py, which proves that the demo never loads the AI client.

Run: python3 tests/test_millionaire_ai_content.py
"""
import json
import os
import sys
import time
import types
import unittest

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

kodi_stubs.install()

REAL_SLEEP = time.sleep
CALLS = {'chat': [], 'tip': []}


def fake_chat(system_prompt, messages, session_id=None):
    CALLS['chat'].append((system_prompt, [dict(m) for m in messages], session_id))
    last = messages[-1]['content']
    if 'WELCOME' in last:
        data = {'moderator_welcome': '[excited] Vítejte u hry!', 'rules_summary': 'Devět otázek.'}
    elif 'QUESTION' in last:
        number = len([m for m in messages if m['role'] == 'user'])
        data = {'moderator_intro': '[dramatically] Otázka {}.'.format(number), 'question': 'Otázka AI {}?'.format(number),
                'options': {'A': 'Správná {}'.format(number), 'B': 'Špatná 1', 'C': 'Špatná 2', 'D': 'Špatná 3'},
                'correct': 'A', 'explanation': '[calm] Protože {}.'.format(number)}
    else:
        data = {'result_announcement': 'Výsledek: ' + ('SPRÁVNĚ' if 'SPRÁVNĚ' in last else 'ŠPATNĚ'),
                'transition_text': 'Dál.'}
    return data, {'input_tokens': 1, 'output_tokens': 1}


def fake_tip(**kwargs):
    CALLS['tip'].append(kwargs)
    return '  Tip od AI.  '


ai_module = types.ModuleType('wcs.ai.AIHandler')
ai_module.generate_millionaire_chat = fake_chat
ai_module.generate_director_tip = fake_tip
sys.modules['wcs.ai.AIHandler'] = ai_module

from wcs.games.MillionaireGame import MillionaireGameController      # noqa: E402


def setUpModule():
    time.sleep = lambda seconds: REAL_SLEEP(0)


def tearDownModule():
    time.sleep = REAL_SLEEP


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


class AiPathTests(unittest.TestCase):
    def setUp(self):
        CALLS['chat'].clear()
        CALLS['tip'].clear()
        self.texts, self.buttons = [], []
        self.game = MillionaireGameController(title='Matrix', plot='Neo.', genre='Sci-fi', media_type='movie')
        self.game.set_ui_callbacks(
            update_display=self.texts.append, update_buttons=lambda *labels: self.buttons.append(labels),
            update_sidebar=lambda: None, show_error=self.texts.append, set_focus=lambda index: None,
            close_dialog=lambda: None, update_millionaire_ladder=lambda: None,
            set_answer_button_colors=lambda *args: None)

    def begin(self):
        self.game.start_specific_game()
        self.game.handle_game_choice(0)
        wait_for(lambda: self.game.game_state == 'welcome_screen', 'welcome')
        self.game.handle_game_choice(0)
        wait_for(lambda: self.game.game_state == 'playing', 'first question')

    def test_the_game_uses_the_ai_source_by_default(self):
        self.assertEqual(self.game.content.name, 'ai')
        self.assertEqual(self.game.content.rules_note, 'AI vytváří unikátní otázky')
        self.assertEqual(self.game.content.badge, '')

    def test_welcome_question_and_feedback_come_from_the_chat(self):
        self.begin()
        self.assertIn('Vítejte u hry!', '\n'.join(self.texts))            # audio tags stripped for the screen
        self.assertNotIn('[excited]', '\n'.join(self.texts))
        # the first question is asked together with the welcome: its request carries the welcome only if that was
        # already back (the fake answers at once), so the number the fake gives it is 1 or 2
        self.assertIn(self.game.current_question['question'], ('Otázka AI 1?', 'Otázka AI 2?'))
        right = 'Správná ' + self.game.current_question['question'][-2]
        before = len(self.buttons)
        self.game.answer_question(self.game.current_question['correct'])      # options are shuffled by the source
        wait_for(lambda: len(self.buttons) > before and self.buttons[-1][0] == 'Další otázka', 'feedback buttons')
        self.assertIn('Výsledek: SPRÁVNĚ', '\n'.join(self.texts))
        # one continuous chat: the system prompt is fixed, the history only grows, one session id throughout
        system_prompts = {call[0] for call in CALLS['chat']}
        sessions = {call[2] for call in CALLS['chat']}
        self.assertEqual((len(system_prompts), len(sessions)), (1, 1))
        self.assertTrue(next(iter(sessions)).startswith('millionaire_'))
        feedback = next(call for call in CALLS['chat'] if 'SPRÁVNĚ' in call[1][-1]['content'])
        roles = [m['role'] for m in feedback[1]]
        self.assertEqual(roles, ['user', 'assistant', 'user', 'assistant', 'user'])      # welcome, question, feedback
        self.assertIn('Matrix', next(iter(system_prompts)))
        self.assertIn(right, feedback[1][-1]['content'])
        # the next question is asked right after the answer, alongside the feedback
        self.assertTrue(any(call[1][-1]['content'].startswith('Kolo 2/9,') for call in CALLS['chat']))

    def test_a_new_game_starts_a_fresh_chat(self):
        self.begin()
        first_session, calls_before = CALLS['chat'][0][2], len(CALLS['chat'])
        self.game.start_millionaire_game()
        wait_for(lambda: self.game.game_state == 'welcome_screen', 'welcome of the second game')
        first_call = CALLS['chat'][calls_before]
        self.assertNotEqual(first_call[2], first_session)
        self.assertEqual(len(first_call[1]), 1, 'the history of the old game was dropped')

    def test_the_tip_comes_from_the_ai(self):
        self.begin()
        self.game.use_tip_lifeline()
        wait_for(lambda: CALLS['tip'] and 'Tip od AI.' in self.texts[-1], 'the tip')
        call = CALLS['tip'][0]
        question = self.game.current_question['question']
        self.assertEqual((call['media_type'], call['title'], call['question']), ('movie', 'Matrix', question))
        self.assertIn('Správná ' + question[-2], call['options'])
        self.assertTrue(call['session_id'].startswith('millionaire_'))

    def test_a_failing_ai_shows_the_error_screen(self):
        def broken(*args, **kwargs):
            raise RuntimeError('no credit')
        ai_module.generate_millionaire_chat = broken
        try:
            self.game.start_specific_game()
            self.game.handle_game_choice(0)
            wait_for(lambda: self.game.game_state == 'error', 'the error screen')
            self.assertIn('no credit', self.texts[-1])
            self.assertEqual(self.buttons[-1][:2], ('Zkusit znovu', 'Ukončit hru'))
        finally:
            ai_module.generate_millionaire_chat = fake_chat


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