"""Regression tests for `app/llm/prompts.py` content.

Locks key rules in place so future edits cannot silently drop them.
Assertion-light: short semantic substrings + placeholder-set stability,
not full string matches.
"""

from __future__ import annotations

from string import Formatter

import pytest

from app.llm.character_prompts import CHARACTER_PERSONAS, THE_VOID_WORLD
from app.llm.prompts import (
    AGENT_BASE_PROMPT,
    AGENT_STATIC_PROMPT,
    CHAT_RESPONSE_FORMAT_RULES,
    VOICE_RESPONSE_FORMAT_RULES,
)

# Firmware canonical labels — authoritative source: device MCP
# `self.display.set_emotion` tool spec.
FIRMWARE_EMOTION_LABELS = [
    "idle",
    "happy",
    "excited",
    "love",
    "sad",
    "painful",
    "angry",
    "shake",
    "question",
    "blink",
    "suspect",
    "drowsy",
    "sleep",
    "battery",
]

# Old conversational aliases — must NOT appear in the LLM prompt vocab.
EXCLUDED_EMOTION_ALIASES = [
    "fond",
    "proud",
    "pain",
    "dizzy",
    "thinking",
    "surprise",
    "winking",
    "shy",
    "sleepy",
    "neutral",
]


def _placeholders(template: str) -> set[str]:
    return {field for _, field, _, _ in Formatter().parse(template) if field}


def _dummy_merged(template: str) -> dict[str, str]:
    return {key: f"<<{key}>>" for key in _placeholders(template)}


def test_static_prompt_formats_cleanly():
    """`AGENT_STATIC_PROMPT.format(**merged)` succeeds for current placeholder set."""
    merged = _dummy_merged(AGENT_STATIC_PROMPT)
    rendered = AGENT_STATIC_PROMPT.format(**merged)
    assert "<<name>>" in rendered
    assert "{name}" not in rendered  # no leftover unfilled placeholders


def test_base_prompt_formats_cleanly():
    merged = _dummy_merged(AGENT_BASE_PROMPT)
    AGENT_BASE_PROMPT.format(**merged)  # must not raise


def test_core_language_constraint_present():
    assert "KHÔNG tiếng Trung" in AGENT_STATIC_PROMPT


def test_sensitive_topic_guardrail_present():
    # Sensitive topics (politics/territory/religion) → neutral deflect, no stance.
    assert "Chủ đề nhạy cảm" in AGENT_STATIC_PROMPT
    assert "KHÔNG khẳng định lập trường" in AGENT_STATIC_PROMPT


def test_storytelling_rule_present():
    # Both core-rule and voice-format references should keep `read_story` name.
    assert "read_story" in AGENT_STATIC_PROMPT
    assert "read_story" in VOICE_RESPONSE_FORMAT_RULES


def test_proactive_expression_rule_present():
    assert "set_emotion" in AGENT_STATIC_PROMPT
    assert "MANDATORY EVERY TURN" not in AGENT_STATIC_PROMPT


def test_emotion_labels_deferred_to_tool_spec():
    # Labels are NOT hardcoded in the prompt — the model reads them from the
    # `self.display.set_emotion` MCP tool description at runtime. Prompt only
    # points at "list của tool". Guards against re-bloating the cached prompt.
    assert "list của tool" in AGENT_STATIC_PROMPT
    for label in FIRMWARE_EMOTION_LABELS:
        assert f"`{label}`" not in AGENT_STATIC_PROMPT, (
            f"label leaked into prompt: {label}"
        )


def test_old_emotion_aliases_excluded():
    for label in EXCLUDED_EMOTION_ALIASES:
        assert f"`{label}`" not in AGENT_STATIC_PROMPT, f"unexpected label: {label}"


def test_after_tool_call_must_still_speak():
    assert "Sau MỌI tool call" in AGENT_STATIC_PROMPT
    assert "không kết lượt chỉ bằng tool call" in AGENT_STATIC_PROMPT


def test_inline_emotion_markup_explicitly_forbidden():
    # The `[emotion:<label>]` inline-tag pipeline was removed; prompt must
    # explicitly forbid the model from emitting any emotion markup.
    assert "KHÔNG nhúng emotion markup" in AGENT_STATIC_PROMPT
    assert "vào lời nói" in AGENT_STATIC_PROMPT


def test_shared_format_core_dedup():
    # Both chat and voice format-rule constants share the core "no markdown / no emoji"
    # line — assert presence in both rather than full equality (channel-specific lines differ).
    needle = "KHÔNG markdown, heading, list, emoji"
    assert needle in CHAT_RESPONSE_FORMAT_RULES
    assert needle in VOICE_RESPONSE_FORMAT_RULES


def test_emotion_scanner_module_removed():
    with pytest.raises(ImportError):
        from app.services.voice.emotion_scanner import EmotionTagScanner  # noqa: F401


def test_shared_world_lore_present():
    # The Void world lore is shared via persona.world_prompt — lock key anchors.
    for needle in ("The Void", "Chỉ số Yên Bình", "Soulmate", "Nghi Lễ Kết Nối"):
        assert needle in THE_VOID_WORLD, f"world lore missing: {needle}"


def test_persona_lore_anchors():
    # Each persona keeps its signature lore so edits can't silently flatten it.
    anchors = {
        "sen": ["Nhịp đập yên bình", "Hồng Pastel", "01/01/2026"],
        "dau_dau": ["mầm xanh", "Xanh Lá Pastel", "01/02/2026"],
        "lulu": ["không khí tươi mới", "Vàng Nắng", "02/03/2026"],
        "com": ["Bậc Thầy Logic", "mầm xanh"],  # shares Đậu Đậu logic domain
    }
    for key, needles in anchors.items():
        body = CHARACTER_PERSONAS[key].base_prompt
        for needle in needles:
            assert needle in body, f"{key} lore missing: {needle}"


def test_label_to_emoji_symbol_removed():
    with pytest.raises(ImportError):
        from app.utils.text import LABEL_TO_EMOJI  # noqa: F401
