"""
Tests for cli/acl_cli/tui/agent.py (S16-C2).

Uses FakeStreamResponse / FakeAsyncClient to test SSE parsing
without hitting any real server.
"""
from __future__ import annotations

import json
from typing import Any, AsyncIterator

import httpx
import pytest

from acl_cli.client import ACLClient
from acl_cli.tui.agent import process_with_ai


# ── Fakes ─────────────────────────────────────────────────────────────────────


class FakeStreamResponse:
    """Mimics httpx.Response inside an async stream context."""

    def __init__(self, status_code: int, sse_events: list[dict[str, Any]]) -> None:
        self.status_code = status_code
        self._lines = [f"data: {json.dumps(e)}" for e in sse_events]

    async def aiter_lines(self) -> AsyncIterator[str]:
        for line in self._lines:
            yield line

    async def aclose(self) -> None:
        pass

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        pass


class FakeAsyncClient:
    """Replaces httpx.AsyncClient; returns a FakeStreamResponse."""

    def __init__(self, fake_response: FakeStreamResponse) -> None:
        self._response = fake_response

    def stream(self, method: str, url: str, **kwargs) -> FakeStreamResponse:
        return self._response

    async def __aenter__(self):
        return self

    async def __aexit__(self, *args):
        pass


# ── Helpers ───────────────────────────────────────────────────────────────────


def _make_client() -> ACLClient:
    return ACLClient(api_key="test-key", base_url="http://localhost:8000")


def _sse_events(*types: str) -> list[dict[str, Any]]:
    """Build minimal SSE event dicts from type names."""
    events: list[dict[str, Any]] = []
    for t in types:
        if t == "tool_start":
            events.append({"type": "tool_start", "tool": "browser__navigate", "params": {"url": "https://example.com"}})
        elif t == "tool_done":
            events.append({"type": "tool_done", "tool": "browser__navigate", "result": {"title": "Example Domain"}})
        elif t == "response":
            events.append({"type": "response", "text": "Открыл Example Domain"})
        elif t == "error":
            events.append({"type": "error", "message": "что-то пошло не так"})
        elif t == "done":
            events.append({"type": "done"})
    return events


# ── Tests ─────────────────────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_process_text_response(monkeypatch):
    """Simple text response yields (response, ...) then (done, ...)."""
    events = _sse_events("response", "done")
    fake_resp = FakeStreamResponse(200, events)
    fake_client = FakeAsyncClient(fake_resp)

    monkeypatch.setattr("acl_cli.tui.agent.httpx.AsyncClient", lambda **kw: fake_client)

    collected = []
    async for etype, edata in process_with_ai("Привет", "user-1", _make_client()):
        collected.append((etype, edata))

    assert len(collected) == 2
    assert collected[0][0] == "response"
    assert collected[0][1]["text"] == "Открыл Example Domain"
    assert collected[1][0] == "done"


@pytest.mark.asyncio
async def test_process_with_ai_yields_events(monkeypatch):
    """tool_start → tool_done → response → done — full flow."""
    events = _sse_events("tool_start", "tool_done", "response", "done")
    fake_resp = FakeStreamResponse(200, events)
    fake_client = FakeAsyncClient(fake_resp)

    monkeypatch.setattr("acl_cli.tui.agent.httpx.AsyncClient", lambda **kw: fake_client)

    types = []
    async for etype, _ in process_with_ai(
        text="открой example.com",
        user_id="test",
        client=_make_client(),
    ):
        types.append(etype)

    assert "tool_start" in types
    assert "tool_done" in types
    assert "response" in types
    assert "done" in types


@pytest.mark.asyncio
async def test_process_http_error(monkeypatch):
    """Non-200 status yields a single error tuple."""
    fake_resp = FakeStreamResponse(500, [])
    fake_client = FakeAsyncClient(fake_resp)

    monkeypatch.setattr("acl_cli.tui.agent.httpx.AsyncClient", lambda **kw: fake_client)

    collected = []
    async for etype, edata in process_with_ai("test", "user-1", _make_client()):
        collected.append((etype, edata))

    assert len(collected) == 1
    assert collected[0][0] == "error"
    assert "500" in collected[0][1]["message"]


@pytest.mark.asyncio
async def test_process_malformed_json(monkeypatch):
    """Malformed SSE data lines are silently skipped."""

    class BrokenStreamResponse(FakeStreamResponse):
        def __init__(self):
            self.status_code = 200

        async def aiter_lines(self):
            yield "data: {not-valid-json}"
            yield f"data: {json.dumps({'type': 'done'})}"

    fake_client = FakeAsyncClient(BrokenStreamResponse())
    monkeypatch.setattr("acl_cli.tui.agent.httpx.AsyncClient", lambda **kw: fake_client)

    collected = []
    async for etype, edata in process_with_ai("test", "user-1", _make_client()):
        collected.append((etype, edata))

    assert len(collected) == 1
    assert collected[0][0] == "done"


@pytest.mark.asyncio
async def test_process_empty_data_lines(monkeypatch):
    """Empty 'data: ' lines are skipped."""

    class EmptyDataResponse(FakeStreamResponse):
        def __init__(self):
            self.status_code = 200

        async def aiter_lines(self):
            yield "data: "
            yield "data:   "
            yield f"data: {json.dumps({'type': 'done'})}"

    fake_client = FakeAsyncClient(EmptyDataResponse())
    monkeypatch.setattr("acl_cli.tui.agent.httpx.AsyncClient", lambda **kw: fake_client)

    collected = []
    async for etype, edata in process_with_ai("test", "user-1", _make_client()):
        collected.append((etype, edata))

    assert len(collected) == 1
    assert collected[0][0] == "done"


@pytest.mark.asyncio
async def test_process_with_session_and_site(monkeypatch):
    """session_id and site_url are passed in the payload."""
    captured_kwargs: dict = {}

    class SpyStreamResponse(FakeStreamResponse):
        def __init__(self):
            self.status_code = 200

        async def aiter_lines(self):
            yield f"data: {json.dumps({'type': 'done'})}"

    class SpyClient:
        def stream(self, method, url, **kwargs):
            captured_kwargs.update(kwargs)
            return SpyStreamResponse()

        async def __aenter__(self):
            return self

        async def __aexit__(self, *args):
            pass

    monkeypatch.setattr("acl_cli.tui.agent.httpx.AsyncClient", lambda **kw: SpyClient())

    collected = []
    async for etype, edata in process_with_ai(
        "test", "user-1", _make_client(),
        session_id="sess-123",
        site_url="https://example.com",
    ):
        collected.append((etype, edata))

    assert collected[0][0] == "done"
    payload = captured_kwargs["json"]
    assert payload["session_id"] == "sess-123"
    assert payload["site_url"] == "https://example.com"
    assert captured_kwargs["headers"]["Accept"] == "text/event-stream"


@pytest.mark.asyncio
async def test_process_error_event(monkeypatch):
    """Server-sent error event is properly yielded."""
    events = _sse_events("error", "done")
    fake_resp = FakeStreamResponse(200, events)
    fake_client = FakeAsyncClient(fake_resp)

    monkeypatch.setattr("acl_cli.tui.agent.httpx.AsyncClient", lambda **kw: fake_client)

    collected = []
    async for etype, edata in process_with_ai("test", "user-1", _make_client()):
        collected.append((etype, edata))

    assert collected[0][0] == "error"
    assert collected[0][1]["message"] == "что-то пошло не так"
    assert collected[1][0] == "done"
