"""Tests for kokoro_chat.engine.background — pure helper functions."""
from __future__ import annotations

import pytest

from kokoro_chat.engine.background import (
    build_event_analysis_prompt,
    build_extract_user_info_prompt,
    build_process_memory_prompt,
    build_summarize_prompt,
    may_contain_personal_info,
    parse_event_impact,
    parse_extracted_user_info,
    parse_memory_summary,
    strip_app_markup,
)
from kokoro_chat.storage.protocol import ChatMessage


# ---------------------------------------------------------------------------
# may_contain_personal_info
# ---------------------------------------------------------------------------

class TestMayContainPersonalInfo:

    @pytest.mark.parametrize("text", [
        "我叫小明",
        "my name is Wade",
        "我的工作是工程師",
        "今天生日快樂",
        "我喜歡打球",
        "最近壓力很大",
        "我養了一隻貓",
        "住在台北",
    ])
    def test_matching_hints(self, text: str) -> None:
        assert may_contain_personal_info(text) is True

    @pytest.mark.parametrize("text", [
        "你好",
        "hello world",
        "天氣真好",
        "謝謝你的回覆",
    ])
    def test_no_match(self, text: str) -> None:
        assert may_contain_personal_info(text) is False

    def test_case_insensitive(self) -> None:
        assert may_contain_personal_info("My Name is Wade") is True
        assert may_contain_personal_info("I LIVE in Taipei") is True
        assert may_contain_personal_info("MY JOB is coding") is True

    def test_empty_string(self) -> None:
        assert may_contain_personal_info("") is False


# ---------------------------------------------------------------------------
# strip_app_markup
# ---------------------------------------------------------------------------

class TestStripAppMarkup:

    def test_remove_memo_user(self) -> None:
        assert strip_app_markup("<memo_user>data</memo_user>hello") == "hello"

    def test_remove_memo_global(self) -> None:
        assert strip_app_markup("<memo_global>g</memo_global>world") == "world"

    def test_remove_think(self) -> None:
        assert strip_app_markup("<think>internal</think>response") == "response"

    def test_multiple_tags(self) -> None:
        text = "<memo_user>u</memo_user><think>t</think>clean"
        assert strip_app_markup(text) == "clean"

    def test_multiline_content(self) -> None:
        text = "<think>line1\nline2\nline3</think>result"
        assert strip_app_markup(text) == "result"

    def test_no_tags_passthrough(self) -> None:
        assert strip_app_markup("just plain text") == "just plain text"

    def test_empty_string(self) -> None:
        assert strip_app_markup("") == ""

    def test_nested_looking_content(self) -> None:
        text = "<memo_user>some <b>html</b> inside</memo_user>ok"
        assert strip_app_markup(text) == "ok"

    def test_strips_surrounding_whitespace(self) -> None:
        text = "  <think>x</think>  hello  "
        assert strip_app_markup(text) == "hello"


# ---------------------------------------------------------------------------
# build_summarize_prompt
# ---------------------------------------------------------------------------

def _make_messages(n: int) -> list[ChatMessage]:
    """Create *n* alternating user/assistant messages."""
    return [
        ChatMessage(
            role="user" if i % 2 == 0 else "assistant",
            content=f"msg-{i}",
        )
        for i in range(n)
    ]


class TestBuildSummarizePrompt:

    async def test_fewer_than_10_returns_empty(self) -> None:
        result = await build_summarize_prompt("Aria", _make_messages(9), "")
        assert result == ""

    async def test_exactly_10_returns_empty(self) -> None:
        # messages[:-10] is empty, so nothing to compress
        result = await build_summarize_prompt("Aria", _make_messages(10), "")
        # With exactly 10 messages, to_compress is empty so dialogue_text
        # is empty, but the prompt itself is still returned.
        # Actually len(messages) == 10 >= 10, so it proceeds;
        # to_compress = messages[:-10] = [] -> dialogue_text = ""
        # The prompt is still generated (not empty).
        assert isinstance(result, str)
        assert len(result) > 0

    async def test_more_than_10_contains_character_name(self) -> None:
        msgs = _make_messages(15)
        result = await build_summarize_prompt("Aria", msgs, "")
        assert "Aria" in result

    async def test_more_than_10_formats_user_messages(self) -> None:
        msgs = _make_messages(15)
        result = await build_summarize_prompt("Aria", msgs, "")
        # First 5 messages should be compressed (15-10=5)
        assert "msg-0" in result

    async def test_existing_summary_included(self) -> None:
        msgs = _make_messages(15)
        result = await build_summarize_prompt("Aria", msgs, "prior summary text")
        assert "prior summary text" in result

    async def test_no_existing_summary(self) -> None:
        msgs = _make_messages(15)
        result = await build_summarize_prompt("Aria", msgs, "")
        assert "之前的摘要：" not in result

    async def test_user_role_formatted_as_yonghu(self) -> None:
        msgs = [ChatMessage(role="user", content="hi")] * 12
        result = await build_summarize_prompt("Aria", msgs, "")
        assert "用戶" in result

    async def test_assistant_role_formatted_as_character_name(self) -> None:
        msgs = [ChatMessage(role="assistant", content="hello")] * 12
        result = await build_summarize_prompt("Luna", msgs, "")
        assert "Luna" in result

    async def test_at_most_20_compressed(self) -> None:
        msgs = _make_messages(60)
        result = await build_summarize_prompt("Aria", msgs, "")
        # to_compress = messages[:-10] = 50 messages, take last 20
        # So msg-30 through msg-49 should appear, but msg-0 should not.
        assert "msg-0" not in result
        assert "msg-30" in result
        assert "msg-49" in result


# ---------------------------------------------------------------------------
# parse_extracted_user_info
# ---------------------------------------------------------------------------

class TestParseExtractedUserInfo:

    @pytest.mark.parametrize("raw, expected", [
        (
            '[{"key": "user_name", "value": "Wade"}]',
            [{"key": "user_name", "value": "Wade"}],
        ),
        (
            '[{"key": "user_name", "value": "Wade"}, {"key": "has_pet", "value": "Mochi"}]',
            [{"key": "user_name", "value": "Wade"}, {"key": "has_pet", "value": "Mochi"}],
        ),
        ("[]", []),
    ])
    def test_valid_json(self, raw: str, expected: list) -> None:
        assert parse_extracted_user_info(raw) == expected

    def test_with_markdown_code_block(self) -> None:
        raw = '```json\n[{"key": "user_name", "value": "Wade"}]\n```'
        assert parse_extracted_user_info(raw) == [{"key": "user_name", "value": "Wade"}]

    @pytest.mark.parametrize("raw", [
        "not json at all",
        "{invalid",
        "",
    ])
    def test_invalid_json_returns_empty(self, raw: str) -> None:
        assert parse_extracted_user_info(raw) == []

    def test_non_list_returns_empty(self) -> None:
        assert parse_extracted_user_info('{"key": "a", "value": "b"}') == []

    def test_filters_missing_key(self) -> None:
        raw = '[{"key": "", "value": "Wade"}, {"key": "age", "value": "25"}]'
        result = parse_extracted_user_info(raw)
        assert result == [{"key": "age", "value": "25"}]

    def test_filters_missing_value(self) -> None:
        raw = '[{"key": "user_name", "value": ""}, {"key": "age", "value": "25"}]'
        result = parse_extracted_user_info(raw)
        assert result == [{"key": "age", "value": "25"}]

    def test_filters_whitespace_only_key_value(self) -> None:
        raw = '[{"key": "  ", "value": "  "}]'
        assert parse_extracted_user_info(raw) == []

    def test_strips_whitespace(self) -> None:
        raw = '[{"key": " user_name ", "value": " Wade "}]'
        result = parse_extracted_user_info(raw)
        assert result == [{"key": "user_name", "value": "Wade"}]


# ---------------------------------------------------------------------------
# parse_memory_summary
# ---------------------------------------------------------------------------

class TestParseMemorySummary:

    def test_valid_json(self) -> None:
        raw = '{"summary": "User likes cats", "importance": 0.8}'
        assert parse_memory_summary(raw) == ("User likes cats", 0.8)

    def test_with_markdown_code_block(self) -> None:
        raw = '```json\n{"summary": "event", "importance": 0.6}\n```'
        assert parse_memory_summary(raw) == ("event", 0.6)

    @pytest.mark.parametrize("raw_importance, clamped", [
        (1.5, 1.0),
        (2.0, 1.0),
        (-0.1, 0.0),
        (-5.0, 0.0),
    ])
    def test_importance_clamping(self, raw_importance: float, clamped: float) -> None:
        raw = f'{{"summary": "test", "importance": {raw_importance}}}'
        summary, importance = parse_memory_summary(raw)
        assert summary == "test"
        assert importance == clamped

    def test_invalid_json_fallback(self) -> None:
        raw = "this is not json"
        assert parse_memory_summary(raw) == (raw, 0.5)

    def test_missing_key_fallback(self) -> None:
        raw = '{"text": "hello"}'
        assert parse_memory_summary(raw) == (raw, 0.5)

    def test_boundary_values(self) -> None:
        assert parse_memory_summary('{"summary": "a", "importance": 0.0}') == ("a", 0.0)
        assert parse_memory_summary('{"summary": "b", "importance": 1.0}') == ("b", 1.0)


# ---------------------------------------------------------------------------
# parse_event_impact
# ---------------------------------------------------------------------------

class TestParseEventImpact:

    def test_valid_json(self) -> None:
        raw = '{"affection_delta": 5, "trust_delta": 3, "new_mood": "happy"}'
        result = parse_event_impact(raw)
        assert result is not None
        assert result["affection_delta"] == 5

    def test_json_with_extra_text_regex(self) -> None:
        raw = 'Here is the analysis:\n{"affection_delta": -10, "trust_delta": -5, "new_mood": "angry"}\nEnd.'
        result = parse_event_impact(raw)
        assert result is not None
        assert result["affection_delta"] == -10
        assert result["new_mood"] == "angry"

    def test_empty_string_returns_none(self) -> None:
        assert parse_event_impact("") is None

    def test_no_json_returns_none(self) -> None:
        assert parse_event_impact("no json here at all") is None

    def test_json_without_affection_delta_key(self) -> None:
        # Direct json.loads succeeds, returns the dict even without affection_delta
        raw = '{"trust_delta": 5}'
        result = parse_event_impact(raw)
        assert result == {"trust_delta": 5}

    def test_malformed_json_no_affection_delta_returns_none(self) -> None:
        raw = "blah {not valid json} blah"
        assert parse_event_impact(raw) is None


# ---------------------------------------------------------------------------
# build_extract_user_info_prompt
# ---------------------------------------------------------------------------

class TestBuildExtractUserInfoPrompt:

    def test_returns_nonempty_string(self) -> None:
        result = build_extract_user_info_prompt("hi", "hello", "Aria")
        assert isinstance(result, str)
        assert len(result) > 0

    def test_contains_inputs(self) -> None:
        result = build_extract_user_info_prompt("my message", "bot reply", "Luna")
        assert "my message" in result
        assert "bot reply" in result
        assert "Luna" in result


# ---------------------------------------------------------------------------
# build_process_memory_prompt
# ---------------------------------------------------------------------------

class TestBuildProcessMemoryPrompt:

    def test_returns_nonempty_string(self) -> None:
        result = build_process_memory_prompt("user says", "bot says", "Aria")
        assert isinstance(result, str)
        assert len(result) > 0

    def test_contains_inputs(self) -> None:
        result = build_process_memory_prompt("user msg", "assistant msg", "Kira")
        assert "user msg" in result
        assert "assistant msg" in result
        assert "Kira" in result


# ---------------------------------------------------------------------------
# build_event_analysis_prompt
# ---------------------------------------------------------------------------

class TestBuildEventAnalysisPrompt:

    def test_returns_nonempty_string(self) -> None:
        result = build_event_analysis_prompt(
            "Aria", "gentle", 50, 60, "neutral", "user gave a gift",
        )
        assert isinstance(result, str)
        assert len(result) > 0

    def test_contains_all_inputs(self) -> None:
        result = build_event_analysis_prompt(
            "Luna", "shy", 30, 40, "happy", "user told a joke",
        )
        assert "Luna" in result
        assert "shy" in result
        assert "30" in result
        assert "40" in result
        assert "happy" in result
        assert "user told a joke" in result
