"""Tests for kokoro_chat.prompt.sections — shared section builders."""
from __future__ import annotations

import pytest

from kokoro_chat.character.model import Character
from kokoro_chat.prompt.sections import (
    _select_speech_pattern,
    build_anti_patterns_section,
    build_character_section,
    build_chat_mode_section,
    build_core_character_sections,
    build_mes_example_section,
    build_personality_summary_section,
    build_scene_section,
    build_speech_section,
    build_traits_section,
)

# ---------------------------------------------------------------------------
# Shared fixtures
# ---------------------------------------------------------------------------

CHAT_MODES = {
    "short": "回覆保持簡短，1-3 句話。",
    "long": "回覆 200 字以上，包含場景描寫。",
}


@pytest.fixture
def v2_char():
    """V2 character with description field set."""
    return Character(
        id="v2",
        name="AkiraV2",
        description="A cheerful high-school student who loves music.",
        personality_summary="warm, energetic, curious",
        likes=["music", "cats"],
        dislikes=["homework"],
        fears=["dark"],
        quirks=["hums while thinking"],
        speech_patterns_v2={"happy": "uwu~", "default": "hmm~"},
        anti_patterns=[
            {"bad": "I don't know", "correct": "Let me check!", "why": "stays in character"},
        ],
        mes_example=[
            {"user": "Hi!", "char": "Hey~!"},
            {"user": "Bye!", "char": "See ya~"},
        ],
        scenario="a school rooftop at sunset",
    )


@pytest.fixture
def legacy_char():
    """Legacy character without description."""
    return Character(
        id="legacy",
        name="Goro",
        personality="stoic",
        background="a retired samurai",
        speaking_style="terse and direct",
        mbti="ISTJ",
        appearance="tall, grey hair, scar on cheek",
        likes=["tea"],
        dislikes=["noise"],
        speech_patterns="speaks in short sentences.",
    )


@pytest.fixture
def empty_char():
    return Character(id="empty", name="Nobody")


# ===========================================================================
# _select_speech_pattern (re-exported from sections.py)
# ===========================================================================


class TestSelectSpeechPatternInSections:
    def test_v2_mood_key(self):
        assert _select_speech_pattern({"happy": "yay!"}, "legacy", "happy") == "yay!"

    def test_v2_default_fallback(self):
        assert _select_speech_pattern({"default": "ok~"}, "legacy", "sad") == "ok~"

    def test_legacy_fallback(self):
        assert _select_speech_pattern({}, "legacy pattern", "happy") == "legacy pattern"

    def test_both_empty(self):
        assert _select_speech_pattern({}, "", "happy") == ""

    def test_strips_whitespace(self):
        assert _select_speech_pattern({"happy": "  hi  "}, "", "happy") == "hi"


# ===========================================================================
# build_character_section
# ===========================================================================


class TestBuildCharacterSection:
    def test_v2_returns_one_section(self, v2_char):
        result = build_character_section(v2_char)
        assert len(result) == 1
        assert result[0].startswith("[角色設定]")
        assert "AkiraV2" in result[0]
        assert "A cheerful high-school student" in result[0]

    def test_v2_no_appearance_section(self, v2_char):
        # V2 never emits a separate [外觀] block
        result = build_character_section(v2_char)
        for s in result:
            assert "[外觀]" not in s

    def test_legacy_returns_char_section_and_appearance(self, legacy_char):
        result = build_character_section(legacy_char)
        assert len(result) == 2
        char_block = result[0]
        appearance_block = result[1]
        assert "[角色設定]" in char_block
        assert "stoic" in char_block
        assert "a retired samurai" in char_block
        assert "terse and direct" in char_block
        assert "ISTJ" in char_block
        assert "[外觀]" in appearance_block
        assert "tall, grey hair" in appearance_block

    def test_legacy_no_appearance_returns_one_section(self):
        char = Character(
            id="x", name="NoApp",
            personality="brave", background="hero", speaking_style="loud", mbti="ENFP",
        )
        result = build_character_section(char)
        assert len(result) == 1
        assert "[角色設定]" in result[0]


# ===========================================================================
# build_personality_summary_section
# ===========================================================================


class TestBuildPersonalitySummarySection:
    def test_with_summary(self, v2_char):
        result = build_personality_summary_section(v2_char)
        assert result is not None
        assert "[性格摘要]" in result
        assert "warm, energetic, curious" in result

    def test_without_summary_returns_none(self, empty_char):
        assert build_personality_summary_section(empty_char) is None


# ===========================================================================
# build_traits_section
# ===========================================================================


class TestBuildTraitsSection:
    def test_all_traits(self, v2_char):
        result = build_traits_section(v2_char)
        assert result is not None
        assert "[特質]" in result
        assert "music" in result
        assert "homework" in result
        assert "dark" in result
        assert "hums while thinking" in result

    def test_empty_char_returns_none(self, empty_char):
        assert build_traits_section(empty_char) is None

    def test_partial_traits_only_present_fields(self):
        char = Character(id="p", name="Partial", likes=["books"])
        result = build_traits_section(char)
        assert result is not None
        assert "books" in result
        assert "討厭" not in result
        assert "害怕" not in result
        assert "小癖好" not in result

    def test_only_dislikes(self):
        char = Character(id="d", name="D", dislikes=["noise"])
        result = build_traits_section(char)
        assert result is not None
        assert "noise" in result
        assert "喜歡" not in result


# ===========================================================================
# build_speech_section
# ===========================================================================


class TestBuildSpeechSection:
    def test_v2_mood_key(self, v2_char):
        result = build_speech_section(v2_char, current_mood="happy")
        assert result is not None
        assert "[語癖]" in result
        assert "uwu~" in result

    def test_v2_default_fallback(self, v2_char):
        result = build_speech_section(v2_char, current_mood="unknown_mood")
        assert result is not None
        assert "hmm~" in result

    def test_legacy_fallback(self, legacy_char):
        result = build_speech_section(legacy_char)
        assert result is not None
        assert "[語癖]" in result
        assert "speaks in short sentences." in result

    def test_both_empty_returns_none(self, empty_char):
        assert build_speech_section(empty_char) is None

    def test_uses_default_mood_when_no_current_mood(self, v2_char):
        # default_mood is "neutral" which is not in speech_patterns_v2, falls to "default"
        result = build_speech_section(v2_char, current_mood=None)
        assert result is not None
        assert "hmm~" in result


# ===========================================================================
# build_anti_patterns_section
# ===========================================================================


class TestBuildAntiPatternsSection:
    def test_empty_returns_none(self, empty_char):
        assert build_anti_patterns_section(empty_char) is None

    def test_valid_patterns(self, v2_char):
        result = build_anti_patterns_section(v2_char)
        assert result is not None
        assert "[不要這樣說]" in result
        assert "I don't know" in result
        assert "Let me check!" in result
        assert "stays in character" in result

    def test_missing_bad_or_correct_filtered(self):
        char = Character(
            id="f", name="Filter",
            anti_patterns=[
                {"bad": "oops", "correct": "", "why": "no correct"},  # filtered
                {"bad": "", "correct": "good", "why": "no bad"},       # filtered
                {"bad": "X", "correct": "Y", "why": "valid"},          # kept
            ],
        )
        result = build_anti_patterns_section(char)
        assert result is not None
        assert "X" in result
        assert "Y" in result
        assert "oops" not in result
        assert "good" not in result

    def test_all_invalid_returns_none(self):
        char = Character(
            id="inv", name="Invalid",
            anti_patterns=[
                {"bad": "", "correct": "", "why": "nothing"},
            ],
        )
        assert build_anti_patterns_section(char) is None


# ===========================================================================
# build_mes_example_section
# ===========================================================================


class TestBuildMesExampleSection:
    def test_empty_returns_none(self, empty_char):
        assert build_mes_example_section(empty_char) is None

    def test_valid_example(self, v2_char):
        result = build_mes_example_section(v2_char, max_examples=1)
        assert result is not None
        assert "[對話風格示例]" in result
        assert "Hi!" in result
        assert "Hey~!" in result

    def test_max_examples_limit(self, v2_char):
        # v2_char has 2 examples; limit to 1
        result = build_mes_example_section(v2_char, max_examples=1)
        assert result is not None
        assert "Bye!" not in result
        assert "See ya~" not in result

    def test_max_examples_allows_all(self, v2_char):
        result = build_mes_example_section(v2_char, max_examples=10)
        assert result is not None
        assert "Hi!" in result
        assert "Bye!" in result

    def test_example_missing_user_or_char_filtered(self):
        char = Character(
            id="f", name="Filter",
            mes_example=[
                {"user": "", "char": "response"},   # filtered
                {"user": "question", "char": ""},   # filtered
                {"user": "Q", "char": "A"},         # kept
            ],
        )
        result = build_mes_example_section(char, max_examples=10)
        assert result is not None
        assert "Q" in result
        assert "A" in result

    def test_all_invalid_examples_returns_none(self):
        char = Character(
            id="inv", name="Inv",
            mes_example=[{"user": "", "char": ""}],
        )
        assert build_mes_example_section(char) is None


# ===========================================================================
# build_chat_mode_section
# ===========================================================================


class TestBuildChatModeSection:
    def test_known_mode(self):
        result = build_chat_mode_section("short", CHAT_MODES)
        assert "[回覆模式]" in result
        assert "1-3 句話" in result

    def test_unknown_mode_falls_to_short(self):
        result = build_chat_mode_section("unknown", CHAT_MODES)
        assert "[回覆模式]" in result
        # Falls back to short
        assert "1-3 句話" in result

    def test_long_mode(self):
        result = build_chat_mode_section("long", CHAT_MODES)
        assert "200 字" in result


# ===========================================================================
# build_scene_section
# ===========================================================================


class TestBuildSceneSection:
    def test_scene_param_takes_priority(self):
        result = build_scene_section("a cafe", "a park")
        assert result is not None
        assert "a cafe" in result
        assert "a park" not in result

    def test_scenario_used_when_no_scene(self):
        result = build_scene_section(None, "a park")
        assert result is not None
        assert "a park" in result

    def test_both_none_returns_none(self):
        assert build_scene_section(None, None) is None

    def test_both_empty_returns_none(self):
        assert build_scene_section("", "") is None

    def test_section_header(self):
        result = build_scene_section("beach", None)
        assert result is not None
        assert "[當前場景]" in result


# ===========================================================================
# build_core_character_sections (integration)
# ===========================================================================


class TestBuildCoreCharacterSections:
    def test_v2_char_full_sections(self, v2_char):
        sections = build_core_character_sections(
            v2_char, "short", CHAT_MODES,
            scene=None, current_mood="happy", max_mes_examples=2,
        )
        combined = "\n\n".join(sections)

        assert "[角色設定]" in combined
        assert "AkiraV2" in combined
        assert "[性格摘要]" in combined
        assert "[特質]" in combined
        assert "[語癖]" in combined
        assert "uwu~" in combined
        assert "[對話風格示例]" in combined
        assert "[不要這樣說]" in combined
        assert "[回覆模式]" in combined
        assert "[當前場景]" in combined
        assert "a school rooftop" in combined

    def test_legacy_char_has_appearance(self, legacy_char):
        sections = build_core_character_sections(legacy_char, "long", CHAT_MODES)
        combined = "\n\n".join(sections)
        assert "[外觀]" in combined
        assert "tall, grey hair" in combined

    def test_empty_char_minimal_sections(self, empty_char):
        sections = build_core_character_sections(empty_char, "short", CHAT_MODES)
        combined = "\n\n".join(sections)
        # Always has character settings and chat mode
        assert "[角色設定]" in combined
        assert "[回覆模式]" in combined
        # No optional sections
        assert "[性格摘要]" not in combined
        assert "[特質]" not in combined
        assert "[語癖]" not in combined
        assert "[對話風格示例]" not in combined
        assert "[不要這樣說]" not in combined
        assert "[當前場景]" not in combined

    def test_section_order(self, v2_char):
        """Sections appear in the documented assembly order."""
        sections = build_core_character_sections(
            v2_char, "short", CHAT_MODES, current_mood="happy", max_mes_examples=1,
        )
        combined = "\n\n".join(sections)
        idx_char = combined.index("[角色設定]")
        idx_summary = combined.index("[性格摘要]")
        idx_traits = combined.index("[特質]")
        idx_speech = combined.index("[語癖]")
        idx_mes = combined.index("[對話風格示例]")
        idx_anti = combined.index("[不要這樣說]")
        idx_mode = combined.index("[回覆模式]")
        idx_scene = combined.index("[當前場景]")

        assert idx_char < idx_summary < idx_traits < idx_speech
        assert idx_speech < idx_mes < idx_anti < idx_mode < idx_scene
