# pyright: reportPrivateUsage=false
"""Mock tests: characters resource (chat, streaming, send, voice)."""

from collections.abc import AsyncIterator
from pathlib import Path
from types import SimpleNamespace
from typing import Any
from unittest.mock import AsyncMock, patch

import pytest

from seaart import AsyncClient
from seaart._internal._schemas.characters.characters_messages import (
    CharacterChatCompletionChunk,
    CharacterChatCompletionEnd,
    CharacterChatCompletionResult,
    CharacterChatVoiceChunk,
    CharacterChatVoiceEnd,
    StreamItem,
    VoiceStreamItem,
)
from seaart.exceptions import StreamInterruptedError

from .conftest import make_ok as _ok


def _chat_data(event: str = "chunk", content: str = "Hello") -> SimpleNamespace:
    """Build a SimpleNamespace mimicking what parse_sse_stream returns."""
    return SimpleNamespace(
        event=event,
        data={
            "id": "c1",
            "object": "chat.completion.chunk",
            "created": 0,
            "model": "gpt",
            "session_id": "s1",
            "msg_id": "m1",
            "choices": [
                {"index": 0, "finish_reason": "", "message": {"role": "text", "content": content}},
            ],
            "usage": {
                "prompt_tokens": 0, "completion_tokens": 0, "search_input_tokens": 0,
                "search_output_tokens": 0, "total_tokens": 0, "total_used": 0, "cost": 0,
            },
        },
        headers={"x-msg-id": "mid1", "x-response-msg-id": "rid1"},
        id=None,
        retry=None,
    )


def _voice_chunk(audio_data: str = "AQID", audio_id: str = "a1") -> SimpleNamespace:
    return SimpleNamespace(
        event="chunk",
        data={"audioData": audio_data, "audio_id": audio_id},
        headers={}, id=None, retry=None,
    )


def _voice_end() -> SimpleNamespace:
    return SimpleNamespace(
        event="end",
        data={"voice": {"id": "v1"}},
        headers={}, id=None, retry=None,
    )


# ── Chat lifecycle ──────────────────────────────────────────────────────────


class TestChatLifecycleMock:
    async def test_delete_chat(self, client: AsyncClient) -> None:
        with patch.object(client, "request_route", new_callable=AsyncMock, return_value=_ok()):
            result = await client.characters.chats.delete("sess-abc")
            assert result.status.code == 10000

    async def test_delete_all(self, client: AsyncClient) -> None:
        with patch.object(client, "request_route", new_callable=AsyncMock, return_value=_ok()):
            result = await client.characters.chats.delete_all("char-1")
            assert result.status.code == 10000

    async def test_list_chats(self, client: AsyncClient) -> None:
        with patch.object(client, "request_route", new_callable=AsyncMock, return_value=_ok({})):
            result = await client.characters.chats.list("char-1", page_size=5)
            assert result.status.code == 10000

    async def test_follow(self, client: AsyncClient) -> None:
        with patch.object(client, "request_route", new_callable=AsyncMock, return_value=_ok({"id": "col1"})):
            result = await client.characters.follow("c1")
            assert result.value.id == "col1"

    async def test_unfollow(self, client: AsyncClient) -> None:
        with patch.object(client, "request_route", new_callable=AsyncMock, return_value=_ok({"id": "col1"})):
            result = await client.characters.unfollow("col1")
            assert result.status.code == 10000


# ── Streaming ───────────────────────────────────────────────────────────────


class TestStreamMock:
    async def test_stream_yields_chunks_and_end(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _chat_data("chunk", "Hello ")
            yield _chat_data("chunk", "world")
            yield _chat_data("end", "")

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            items: list[StreamItem] = []
            async for item in client.characters.chats.stream("hi", "s1"):
                items.append(item)

            assert len(items) == 3
            assert isinstance(items[0], CharacterChatCompletionChunk)
            assert isinstance(items[2], CharacterChatCompletionEnd)
            assert items[0].content == "Hello "

    async def test_stream_stops_after_end(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _chat_data("end", "done")
            yield _chat_data("chunk", "should not appear")

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            items: list[StreamItem] = []
            async for item in client.characters.chats.stream("hi", "s1"):
                items.append(item)
            assert len(items) == 1
            assert isinstance(items[0], CharacterChatCompletionEnd)

    async def test_stream_raw_yields_namespaces(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _chat_data("chunk", "raw1")
            yield _chat_data("end", "raw2")

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            events: list[SimpleNamespace] = []
            async for ns in client.characters.chats.stream_raw("hi", "s1"):
                events.append(ns)
            assert len(events) == 2
            assert events[0].event == "chunk"

    async def test_stream_tourist(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _chat_data("chunk", "tourist hi")
            yield _chat_data("end", "")

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            items: list[StreamItem] = []
            async for item in client.characters.chats.stream_tourist("hello", "s1"):
                items.append(item)
            assert len(items) == 2

    async def test_stream_content_property(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _chat_data("chunk", "test content")
            yield _chat_data("end", "")

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            async for item in client.characters.chats.stream("hi", "s1"):
                if isinstance(item, CharacterChatCompletionChunk):
                    assert item.content == "test content"
                    assert item.my_msg_id == "mid1"
                    assert item.response_msg_id == "rid1"
                    break

    async def test_stream_skips_unparseable_chunks(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield SimpleNamespace(event="chunk", data="not a dict", headers={}, id=None, retry=None)
            yield _chat_data("chunk", "good")
            yield _chat_data("end", "")

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            items: list[StreamItem] = []
            async for item in client.characters.chats.stream("hi", "s1"):
                items.append(item)
            assert len(items) == 2


# ── Send ────────────────────────────────────────────────────────────────────


class TestSendMock:
    async def test_send_collects_full_text(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _chat_data("chunk", "Hello ")
            yield _chat_data("chunk", "world")
            yield _chat_data("end", "")

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            result = await client.characters.chats.send("hi", "s1")
            assert isinstance(result, CharacterChatCompletionResult)
            assert result.text == "Hello world"
            assert isinstance(result.value, CharacterChatCompletionEnd)

    async def test_send_empty_response(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _chat_data("end", "")

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            result = await client.characters.chats.send("hi", "s1")
            assert result.text == ""

    async def test_send_raises_on_no_end(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _chat_data("chunk", "partial")
            return  # noqa: B901

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            with pytest.raises(StreamInterruptedError):
                await client.characters.chats.send("hi", "s1")

    async def test_send_tourist(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _chat_data("chunk", "tourist reply")
            yield _chat_data("end", "")

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            result = await client.characters.chats.send_tourist("hi", "s1")
            assert result.text == "tourist reply"

    async def test_send_tourist_raises_on_no_end(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _chat_data("chunk", "broken")
            return  # noqa: B901

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            with pytest.raises(StreamInterruptedError):
                await client.characters.chats.send_tourist("hi", "s1")


# ── Voice ───────────────────────────────────────────────────────────────────


class TestVoiceMock:
    async def test_stream_voice(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _voice_chunk("AQID")
            yield _voice_end()

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            items: list[VoiceStreamItem] = []
            async for chunk in client.characters.chats.stream_voice(msg_id="m1"):
                items.append(chunk)
            assert len(items) == 2
            assert isinstance(items[0], CharacterChatVoiceChunk)
            assert isinstance(items[1], CharacterChatVoiceEnd)
            assert items[0].audio_data == "AQID"

    async def test_get_audio_returns_bytes(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _voice_chunk("AQID")
            yield _voice_chunk("BAUG")
            yield _voice_end()

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            audio = await client.characters.chats.get_audio(msg_id="m1")
            assert audio is not None
            assert isinstance(audio, bytes)
            assert len(audio) > 0

    async def test_get_audio_returns_none_when_no_chunks(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _voice_end()

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            audio = await client.characters.chats.get_audio(msg_id="m1")
            assert audio is None

    async def test_save_audio(self, client: AsyncClient, tmp_path: Path) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _voice_chunk("AQID")
            yield _voice_end()

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            result = await client.characters.chats.save_audio(
                msg_id="m1", file_path=str(tmp_path / "test.mp3")
            )
            assert result is not None
            assert result.exists()
            assert result.read_bytes() == b"\x01\x02\x03"

    async def test_save_audio_to_directory(self, client: AsyncClient, tmp_path: Path) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _voice_chunk("AQID")
            yield _voice_end()

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            result = await client.characters.chats.save_audio(msg_id="m1", file_path=str(tmp_path))
            assert result is not None
            assert result.name == "voice_m1.mp3"

    async def test_save_audio_returns_none_when_no_audio(self, client: AsyncClient) -> None:
        async def fake_stream(*_a: Any, **_k: Any) -> AsyncIterator[SimpleNamespace]:
            yield _voice_end()

        with patch.object(client, "request_route_stream", side_effect=fake_stream):
            result = await client.characters.chats.save_audio(msg_id="m1")
            assert result is None


# ── Messages list ───────────────────────────────────────────────────────────


class TestMessagesListMock:
    async def test_list_messages(self, client: AsyncClient) -> None:
        data: dict[str, Any] = {
            "items": [
                {
                    "id": "msg1", "content": "hello", "original_content": "hello",
                    "json_content": None, "create_at": 0, "role": 1, "support_voice": 0,
                    "voice_duration": 0, "think_duration": 0, "status": 1, "active_idx": 0,
                    "gpt_model": "test", "file_info": None, "is_active_upload": 0,
                    "search_results": None, "on_line_search": 0, "scene": "", "is_like": 0,
                    "indistinct": 0, "handle_button_text": "", "handle_msg_type": 0,
                },
            ],
            "offset": 0,
            "has_more": False,
        }
        with patch.object(client, "request_route", new_callable=AsyncMock, return_value=_ok(data)):
            result = await client.characters.chats.messages.list("sess-1")
            assert result.status.code == 10000
            assert len(result.value.items) == 1
            assert result.value.items[0].id == "msg1"
