import asyncio

import pytest
from pydantic import Field

from core.protocol import SettingsAction
from sdk.interface import Attachment, MessageChunk, MessageResponse, PlatformError, UserSettings
from sdk.prototype_state import PrototypeStateStore
from sdk.real import RealPlatformClient
from sdk.upstream_agent_api import MsgEventSendFile, MsgEventTextChunk


class FakeChatAgentApi:
    def __init__(self, agent_id: str, base_url: str, chat_id: str) -> None:
        self.agent_id = agent_id
        self.base_url = base_url
        self.chat_id = str(chat_id)
        self.calls: list[str] = []
        self.connect_calls = 0
        self.close_calls = 0

    async def connect(self) -> None:
        self.connect_calls += 1

    async def close(self) -> None:
        self.close_calls += 1

    async def send_message(self, text: str, attachments: list[str] | None = None):
        self.calls.append(text)
        midpoint = len(text) // 2
        yield MsgEventTextChunk(text=text[:midpoint])
        yield MsgEventTextChunk(text=text[midpoint:])


class FakeAgentApiFactory:
    def __init__(self, chat_api_cls=FakeChatAgentApi) -> None:
        self.chat_api_cls = chat_api_cls
        self.created_calls: list[tuple[str, str, str]] = []
        self.instances_by_chat: dict[str, list[FakeChatAgentApi]] = {}

    def __call__(self, agent_id: str, base_url: str, chat_id: str):
        chat_key = str(chat_id)
        chat_api = self.chat_api_cls(agent_id, base_url, chat_key)
        self.created_calls.append((agent_id, base_url, chat_key))
        self.instances_by_chat.setdefault(chat_key, []).append(chat_api)
        return chat_api

    def latest(self, chat_id: str):
        return self.instances_by_chat[str(chat_id)][-1]


class BlockingTracker:
    def __init__(self) -> None:
        self.active_calls = 0
        self.max_active_calls = 0
        self.started = asyncio.Event()
        self.release = asyncio.Event()


class BlockingChatAgentApi(FakeChatAgentApi):
    def __init__(
        self,
        agent_id: str,
        base_url: str,
        chat_id: str,
        *,
        tracker: BlockingTracker,
    ) -> None:
        super().__init__(agent_id, base_url, chat_id)
        self._tracker = tracker

    async def send_message(self, text: str, attachments: list[str] | None = None):
        self.calls.append(text)
        self._tracker.active_calls += 1
        self._tracker.max_active_calls = max(
            self._tracker.max_active_calls,
            self._tracker.active_calls,
        )
        self._tracker.started.set()
        await self._tracker.release.wait()
        self._tracker.active_calls -= 1
        yield MsgEventTextChunk(text=text)


class BlockingAgentApiFactory(FakeAgentApiFactory):
    def __init__(self) -> None:
        super().__init__()
        self.tracker = BlockingTracker()

    def __call__(self, agent_id: str, base_url: str, chat_id: str):
        chat_key = str(chat_id)
        chat_api = BlockingChatAgentApi(
            agent_id,
            base_url,
            chat_key,
            tracker=self.tracker,
        )
        self.created_calls.append((agent_id, base_url, chat_key))
        self.instances_by_chat.setdefault(chat_key, []).append(chat_api)
        return chat_api


class AttachmentTrackingChatAgentApi(FakeChatAgentApi):
    def __init__(self, agent_id: str, base_url: str, chat_id: str) -> None:
        super().__init__(agent_id, base_url, chat_id)
        self.calls: list[tuple[str, list[str] | None]] = []

    async def send_message(self, text: str, attachments: list[str] | None = None):
        self.calls.append((text, attachments))
        yield MsgEventTextChunk(text=text)


class FlakyChatAgentApi(FakeChatAgentApi):
    async def send_message(self, text: str, attachments: list[str] | None = None):
        raise ConnectionError("Connection closed")
        yield


class ReuseSensitiveChatAgentApi(FakeChatAgentApi):
    def __init__(self, agent_id: str, base_url: str, chat_id: str) -> None:
        super().__init__(agent_id, base_url, chat_id)
        self._send_calls = 0

    async def send_message(self, text: str, attachments: list[str] | None = None):
        self.calls.append(text)
        self._send_calls += 1
        if text == "first":
            yield MsgEventTextChunk(text="tool ok")
            return
        if text == "second" and self._send_calls == 1:
            yield MsgEventTextChunk(text="Missing")


class MessageResponseWithAttachments(MessageResponse):
    attachments: list[Attachment] = Field(default_factory=list)


def make_real_platform_client(
    agent_api_cls,
    *,
    prototype_state: PrototypeStateStore | None = None,
) -> RealPlatformClient:
    return RealPlatformClient(
        agent_id="matrix-bot",
        agent_base_url="http://platform-agent:8000",
        agent_api_cls=agent_api_cls,
        prototype_state=prototype_state or PrototypeStateStore(),
        platform="matrix",
    )


@pytest.mark.asyncio
async def test_real_platform_client_get_or_create_user_uses_local_state():
    client = make_real_platform_client(FakeAgentApiFactory())

    first = await client.get_or_create_user("u1", "matrix", "Alice")
    second = await client.get_or_create_user("u1", "matrix")

    assert first.user_id == "usr-matrix-u1"
    assert first.is_new is True
    assert second.user_id == first.user_id
    assert second.is_new is False
    assert second.display_name == "Alice"


@pytest.mark.asyncio
async def test_real_platform_client_send_message_uses_direct_agent_api_per_chat():
    agent_api = FakeAgentApiFactory()
    prototype_state = PrototypeStateStore()
    client = make_real_platform_client(agent_api, prototype_state=prototype_state)

    result = await client.send_message("@alice:example.org", "chat-7", "hello")

    assert result == MessageResponse(
        message_id="@alice:example.org",
        response="hello",
        tokens_used=0,
        finished=True,
    )
    assert agent_api.created_calls == [("matrix-bot", "http://platform-agent:8000", "chat-7")]
    assert agent_api.latest("chat-7").chat_id == "chat-7"
    assert agent_api.latest("chat-7").calls == ["hello"]
    assert agent_api.latest("chat-7").connect_calls == 1
    assert agent_api.latest("chat-7").close_calls == 1
    assert await prototype_state.get_last_tokens_used_for_context("chat-7") == 0


@pytest.mark.asyncio
async def test_real_platform_client_preserves_path_base_url_without_trailing_slash():
    agent_api = FakeAgentApiFactory()
    client = RealPlatformClient(
        agent_id="agent-17",
        agent_base_url="http://lambda.coredump.ru:7000/agent_17",
        agent_api_cls=agent_api,
        prototype_state=PrototypeStateStore(),
        platform="matrix",
    )

    await client.send_message("@alice:example.org", "41", "hello")

    assert agent_api.created_calls == [
        ("agent-17", "http://lambda.coredump.ru:7000/agent_17/", "41")
    ]


@pytest.mark.asyncio
async def test_real_platform_client_forwards_attachments_to_chat_api():
    agent_api = FakeAgentApiFactory(chat_api_cls=AttachmentTrackingChatAgentApi)
    client = make_real_platform_client(agent_api)
    attachment = Attachment(
        url="/agents/7/surfaces/matrix/alice/room/inbox/report.pdf",
        workspace_path="surfaces/matrix/alice/room/inbox/report.pdf",
        mime_type="application/pdf",
        filename="report.pdf",
        size=123,
    )

    result = await client.send_message(
        "@alice:example.org",
        "chat-7",
        "hello",
        attachments=[attachment],
    )

    assert agent_api.latest("chat-7").calls == [
        ("hello", ["surfaces/matrix/alice/room/inbox/report.pdf"])
    ]
    assert result.response == "hello"
    assert result.tokens_used == 0


def test_attachment_paths_normalize_workspace_roots_to_relative_paths():
    attachments = [
        Attachment(workspace_path="/workspace/report.pdf"),
        Attachment(workspace_path="/agents/7/report.csv"),
        Attachment(workspace_path="note.txt"),
    ]

    assert RealPlatformClient._attachment_paths(attachments) == [
        "report.pdf",
        "report.csv",
        "note.txt",
    ]


@pytest.mark.asyncio
async def test_real_platform_client_preserves_send_file_events_in_sync_result(monkeypatch):
    class FileEventAgentApi(AttachmentTrackingChatAgentApi):
        async def send_message(self, text: str, attachments: list[str] | None = None):
            self.calls.append((text, attachments))
            yield MsgEventTextChunk(text="he")
            yield MsgEventSendFile(path="report.pdf")
            yield MsgEventTextChunk(text="llo")

    agent_api = FakeAgentApiFactory(chat_api_cls=FileEventAgentApi)
    client = make_real_platform_client(agent_api)

    monkeypatch.setattr("sdk.real.MessageResponse", MessageResponseWithAttachments)

    result = await client.send_message("@alice:example.org", "chat-7", "hello")

    assert result.response == "hello"
    assert result.tokens_used == 0
    assert result.attachments == [
        Attachment(
            url="report.pdf",
            mime_type="application/octet-stream",
            filename="report.pdf",
            size=None,
            workspace_path="report.pdf",
        )
    ]


@pytest.mark.parametrize(
    ("location", "expected_workspace_path"),
    [
        ("/workspace/report.pdf", "report.pdf"),
        ("/agents/7/report.pdf", "report.pdf"),
        (
            "surfaces/matrix/alice/room/inbox/report.pdf",
            "surfaces/matrix/alice/room/inbox/report.pdf",
        ),
    ],
)
def test_attachment_from_send_file_event_normalizes_shared_volume_paths(
    location: str, expected_workspace_path: str
):
    attachment = RealPlatformClient._attachment_from_send_file_event(
        MsgEventSendFile(path=location)
    )

    assert attachment.url == location
    assert attachment.workspace_path == expected_workspace_path
    assert attachment.filename == "report.pdf"


@pytest.mark.asyncio
async def test_real_platform_client_uses_fresh_agent_connection_per_request():
    agent_api = FakeAgentApiFactory()
    client = make_real_platform_client(agent_api)

    await client.send_message("@alice:example.org", "chat-1", "hello")
    await client.send_message("@alice:example.org", "chat-1", "again")

    assert agent_api.created_calls == [
        ("matrix-bot", "http://platform-agent:8000", "chat-1"),
        ("matrix-bot", "http://platform-agent:8000", "chat-1"),
    ]
    assert [instance.calls for instance in agent_api.instances_by_chat["chat-1"]] == [
        ["hello"],
        ["again"],
    ]
    assert all(instance.connect_calls == 1 for instance in agent_api.instances_by_chat["chat-1"])
    assert all(instance.close_calls == 1 for instance in agent_api.instances_by_chat["chat-1"])


@pytest.mark.asyncio
async def test_real_platform_client_avoids_reuse_sensitive_second_message_loss():
    agent_api = FakeAgentApiFactory(chat_api_cls=ReuseSensitiveChatAgentApi)
    client = make_real_platform_client(agent_api)

    first = await client.send_message("@alice:example.org", "chat-1", "first")
    second = await client.send_message("@alice:example.org", "chat-1", "second")

    assert first.response == "tool ok"
    assert second.response == "Missing"
    assert len(agent_api.instances_by_chat["chat-1"]) == 2


@pytest.mark.asyncio
async def test_real_platform_client_wraps_connection_closed_as_platform_error():
    agent_api = FakeAgentApiFactory(chat_api_cls=FlakyChatAgentApi)
    client = make_real_platform_client(agent_api)

    with pytest.raises(PlatformError, match="Connection closed") as exc_info:
        await client.send_message("@alice:example.org", "chat-1", "hello")

    assert exc_info.value.code == "PLATFORM_CONNECTION_ERROR"
    assert agent_api.latest("chat-1").close_calls == 1


@pytest.mark.asyncio
async def test_real_platform_client_uses_fresh_connection_after_failure():
    class SometimesFlakyAgentApi(FakeChatAgentApi):
        async def send_message(self, text: str, attachments: list[str] | None = None):
            if text == "hello":
                raise ConnectionError("Connection closed")
            self.calls.append(text)
            yield MsgEventTextChunk(text=text)

    agent_api = FakeAgentApiFactory(chat_api_cls=SometimesFlakyAgentApi)
    client = make_real_platform_client(agent_api)

    with pytest.raises(PlatformError, match="Connection closed"):
        await client.send_message("@alice:example.org", "chat-1", "hello")

    result = await client.send_message("@alice:example.org", "chat-1", "again")

    assert result.response == "again"
    assert agent_api.created_calls == [
        ("matrix-bot", "http://platform-agent:8000", "chat-1"),
        ("matrix-bot", "http://platform-agent:8000", "chat-1"),
    ]
    assert agent_api.latest("chat-1").calls == ["again"]


@pytest.mark.asyncio
async def test_real_platform_client_serializes_same_chat_streams_across_send_paths():
    agent_api = BlockingAgentApiFactory()
    client = make_real_platform_client(agent_api)

    async def consume_stream():
        chunks = []
        async for chunk in client.stream_message("@alice:example.org", "chat-1", "hello"):
            chunks.append(chunk)
        return chunks

    stream_task = asyncio.create_task(consume_stream())
    await asyncio.wait_for(agent_api.tracker.started.wait(), timeout=1)

    send_task = asyncio.create_task(client.send_message("@alice:example.org", "chat-1", "again"))
    await asyncio.sleep(0)

    assert len(agent_api.instances_by_chat["chat-1"]) == 1
    assert agent_api.instances_by_chat["chat-1"][0].calls == ["hello"]
    assert agent_api.tracker.max_active_calls == 1

    agent_api.tracker.release.set()
    stream_chunks = await stream_task
    send_result = await send_task

    assert [chunk.delta for chunk in stream_chunks] == ["hello", ""]
    assert send_result.response == "again"
    assert [instance.calls for instance in agent_api.instances_by_chat["chat-1"]] == [
        ["hello"],
        ["again"],
    ]
    assert agent_api.tracker.max_active_calls == 1


@pytest.mark.asyncio
async def test_real_platform_client_creates_distinct_connections_per_chat():
    agent_api = FakeAgentApiFactory()
    client = make_real_platform_client(agent_api)

    await client.send_message("@alice:example.org", "chat-1", "hello")
    await client.send_message("@alice:example.org", "chat-2", "world")

    assert agent_api.created_calls == [
        ("matrix-bot", "http://platform-agent:8000", "chat-1"),
        ("matrix-bot", "http://platform-agent:8000", "chat-2"),
    ]
    assert agent_api.latest("chat-1").calls == ["hello"]
    assert agent_api.latest("chat-2").calls == ["world"]


@pytest.mark.asyncio
async def test_real_platform_client_stream_message_emits_final_tokens_chunk():
    agent_api = FakeAgentApiFactory()
    client = make_real_platform_client(agent_api)

    chunks = []
    async for chunk in client.stream_message("@alice:example.org", "chat-1", "hello"):
        chunks.append(chunk)

    assert chunks == [
        MessageChunk(
            message_id="@alice:example.org",
            delta="he",
            finished=False,
            tokens_used=0,
        ),
        MessageChunk(
            message_id="@alice:example.org",
            delta="llo",
            finished=False,
            tokens_used=0,
        ),
        MessageChunk(
            message_id="@alice:example.org",
            delta="",
            finished=True,
            tokens_used=0,
        ),
    ]
    assert agent_api.created_calls == [("matrix-bot", "http://platform-agent:8000", "chat-1")]
    assert agent_api.latest("chat-1").calls == ["hello"]
    assert agent_api.latest("chat-1").close_calls == 1


@pytest.mark.asyncio
async def test_real_platform_client_settings_are_local():
    client = make_real_platform_client(FakeAgentApiFactory())

    await client.update_settings(
        "usr-matrix-u1",
        SettingsAction(action="toggle_skill", payload={"skill": "browser", "enabled": True}),
    )

    settings = await client.get_settings("usr-matrix-u1")

    assert isinstance(settings, UserSettings)
    assert settings.skills["browser"] is True
    assert settings.skills["web-search"] is True
