import unittest
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch

from app.llm.character_prompts import CHARACTER_PERSONAS
from app.services.session import (
    DEFAULT_CHARACTER_KEY,
    LEGACY_DEFAULT_AGENT_NAME,
    AgentSettings,
    build_agent_settings,
    build_system_prompt,
    make_default_agent_profile,
    resolve_agent_profile,
)


def make_profile(**overrides):
    data = {
        "device_id": "device-1",
        "client_id": "client-1",
        "name": LEGACY_DEFAULT_AGENT_NAME,
        "character_key": None,
        "user_call_name": None,
        "voice_id": None,
        "tts_provider": None,
        "tts_model_id": None,
        "personality": None,
    }
    data.update(overrides)
    return SimpleNamespace(**data)


class CharacterPersonaTests(unittest.IsolatedAsyncioTestCase):
    def test_make_default_agent_profile_uses_mochi_defaults(self):
        with patch(
            "app.services.session.AgentProfile",
            side_effect=lambda **kwargs: SimpleNamespace(**kwargs),
        ):
            profile = make_default_agent_profile("device-1", "client-1")

        self.assertEqual(profile.device_id, "device-1")
        self.assertEqual(profile.client_id, "client-1")
        self.assertEqual(profile.character_key, DEFAULT_CHARACTER_KEY)
        self.assertEqual(profile.name, CHARACTER_PERSONAS["mochi"].name)

    def test_build_agent_settings_uses_character_persona_when_key_is_known(self):
        profile = make_profile(
            name=LEGACY_DEFAULT_AGENT_NAME,
            character_key="dau_dau",
            user_call_name="anh iu",
            personality="Keep answers concise.",
        )

        agent = build_agent_settings(profile)

        self.assertEqual(agent.name, "Đậu Đậu")
        self.assertEqual(agent.character_key, "dau_dau")
        self.assertEqual(agent.user_call_name, "anh iu")
        self.assertIn("treat conversation like a game", agent.base_prompt)
        # Humor and tool style present
        self.assertIn("Dry wit", agent.base_prompt)
        self.assertIn("Never do this", agent.base_prompt)
        self.assertEqual(agent.personality, "Keep answers concise.")

    def test_build_agent_settings_defaults_to_mochi_when_character_key_missing(self):
        profile = make_profile(name="Nova")

        agent = build_agent_settings(profile)

        self.assertEqual(agent.name, "Nova")
        self.assertEqual(agent.character_key, "mochi")
        self.assertIn("tend her garden through a Mirror Well", agent.base_prompt)
        # Humor style present in default character
        self.assertIn("Warm, gentle, observational", agent.base_prompt)

    def test_build_agent_settings_defaults_to_mochi_when_character_key_invalid(self):
        profile = make_profile(
            name=LEGACY_DEFAULT_AGENT_NAME,
            character_key="unknown",
        )

        agent = build_agent_settings(profile)

        self.assertEqual(agent.name, "Mochi")
        self.assertEqual(agent.character_key, "mochi")
        self.assertIn("tend her garden through a Mirror Well", agent.base_prompt)

    def test_build_agent_settings_preserves_custom_name_for_known_character(self):
        profile = make_profile(name="Captain Lulu", character_key="lulu")

        agent = build_agent_settings(profile)

        self.assertEqual(agent.name, "Captain Lulu")
        self.assertEqual(agent.character_key, "lulu")
        self.assertIn("too curious to be stopped by fear", agent.base_prompt)

    async def test_resolve_agent_profile_creates_default_profile_when_missing(self):
        default_profile = make_profile(name="Mochi", character_key="mochi")

        with patch(
            "app.services.session.agent_profile_repo.get_profile",
            new=AsyncMock(return_value=None),
        ), patch(
            "app.services.session.make_default_agent_profile",
            return_value=default_profile,
        ), patch(
            "app.services.session.agent_profile_repo.create_profile",
            new=AsyncMock(),
        ) as create_profile:
            profile, agent = await resolve_agent_profile("device-1", "client-1")

        self.assertEqual(profile.character_key, DEFAULT_CHARACTER_KEY)
        self.assertEqual(profile.name, "Mochi")
        self.assertEqual(agent.character_key, DEFAULT_CHARACTER_KEY)
        self.assertEqual(agent.name, "Mochi")
        create_profile.assert_awaited_once()

    async def test_build_system_prompt_chat_uses_clean_text_rules_and_saved_user_call_name(self):
        persona = CHARACTER_PERSONAS["mochi"]
        agent = AgentSettings(
            name="Mochi",
            user_call_name="anh",
            voice_id="voice-1",
            personality="Be extra gentle today.",
            tts_provider="cartesia",
            tts_model_id=None,
            character_key="mochi",
            base_prompt=persona.base_prompt,
            world_prompt=persona.world_prompt,
            identity_label=persona.identity_label,
        )

        context = {
            "current_time": "10:00",
            "today_date": "2026-03-30",
            "today_weekday": "Monday",
            "lunar_date": "2/3",
            "local_address": "Ho Chi Minh City",
            "weather_info": "Sunny",
            "dynamic_context": "",
        }

        with patch("app.services.session.build_context", new=AsyncMock(return_value=context)):
            prompt = await build_system_prompt(agent, "127.0.0.1", response_mode="chat")

        self.assertIn('Use "anh" as the main way to address the user', prompt)
        self.assertIn("Be extra gentle today.", prompt)
        self.assertIn("Never mix", prompt)
        self.assertIn("Only output the spoken dialogue itself", prompt)
        self.assertIn("Do not use parentheses or brackets for stage directions", prompt)
        self.assertIn("Do not use SSML tags.", prompt)
        self.assertIn("a real being from The Void", prompt)

    async def test_build_system_prompt_voice_keeps_voice_rendering_guidance(self):
        persona = CHARACTER_PERSONAS["lulu"]
        agent = AgentSettings(
            name="Lulu",
            user_call_name=None,
            voice_id="voice-2",
            personality="Stay playful.",
            tts_provider="cartesia",
            tts_model_id=None,
            character_key="lulu",
            base_prompt=persona.base_prompt,
            world_prompt=persona.world_prompt,
            identity_label=persona.identity_label,
        )

        context = {
            "current_time": "10:00",
            "today_date": "2026-03-30",
            "today_weekday": "Monday",
            "lunar_date": "2/3",
            "local_address": "Ho Chi Minh City",
            "weather_info": "Sunny",
            "dynamic_context": "",
        }

        with patch("app.services.session.build_context", new=AsyncMock(return_value=context)):
            prompt = await build_system_prompt(agent, "127.0.0.1", response_mode="voice")

        self.assertIn('If replying in Vietnamese, call the user "anh".', prompt)
        self.assertIn("A single leading emoji or brief expressive marker is allowed", prompt)
        self.assertIn("<emotion value=", prompt)
        self.assertIn("Output spoken dialogue that sounds natural when read aloud", prompt)

    async def test_build_system_prompt_contains_immersion_and_humor_guards(self):
        persona = CHARACTER_PERSONAS["dau_dau"]
        agent = AgentSettings(
            name="Dau Dau",
            user_call_name=None,
            voice_id="voice-1",
            personality="",
            tts_provider="cartesia",
            tts_model_id=None,
            character_key="dau_dau",
            base_prompt=persona.base_prompt,
            world_prompt=persona.world_prompt,
            identity_label=persona.identity_label,
        )

        context = {
            "current_time": "15:00",
            "today_date": "2026-04-03",
            "today_weekday": "Thursday",
            "lunar_date": "2/5",
            "local_address": "Ho Chi Minh City",
            "weather_info": "Cloudy 28C",
            "dynamic_context": "",
        }

        with patch("app.services.session.build_context", new=AsyncMock(return_value=context)):
            prompt = await build_system_prompt(agent, "127.0.0.1", response_mode="voice")

        # Immersion guard present — core assistant-identity forbidden phrases
        self.assertIn("Never use assistant phrases", prompt)
        self.assertIn("diegetic", prompt)
        # Humor rules present
        self.assertIn("Follow the humor style", prompt)
        self.assertIn("drop humor entirely", prompt)
        # Tool-wrapping guard present — retell results in character voice
        self.assertIn("retell the result in your own voice", prompt)
        self.assertIn("never mention tool names", prompt)
        # Character-specific humor in base_prompt
        self.assertIn("Dry wit", prompt)


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