"""An agent reply, rendered end to end.

The steps rewrite touched the one template every bubble goes through and the
frames that fill it, and an agent reply is the case with all of it at once:
reasoning, prose, tool events, a plan card, a project listing. `test_steps.py`
covers the builder as pure functions; this covers the page.
"""

from __future__ import annotations

import json as _json

import pytest
from fastapi.testclient import TestClient
from sqlalchemy import select

from lembas.db.models import KIND_AGENT, Chat, Connection, Message, Model, SshProfile, User
from lembas.services import generation as generation_service
from lembas.services import settings_store


def _text_chunk(text: str) -> dict:
    return {"choices": [{"delta": {"content": text}}]}


def _tool_chunk(name: str, arguments: str) -> dict:
    return {
        "choices": [
            {
                "delta": {
                    "tool_calls": [
                        {
                            "index": 0,
                            "id": "call_1",
                            "function": {"name": name, "arguments": arguments},
                        }
                    ]
                }
            }
        ]
    }


def _stub_stream(rounds):
    calls = {"n": 0}

    async def stream(_endpoint, _payload):
        index = min(calls["n"], len(rounds) - 1)
        calls["n"] += 1
        for chunk in rounds[index]:
            yield chunk

    return stream


@pytest.fixture
def agent_chat(db, registered):
    """An agent chat whose connection exists but has no reachable host.

    Deliberately unreachable: what is under test is the *rendering* of a reply
    that called a tool, and a tool that fails still produces an event, which is
    the thing the transcript has to lay out.
    """
    settings_store.update(
        db,
        {"enabled": True, "background_enabled": True, "terminal_enabled": True},
        key=settings_store.AGENTS,
    )
    user = db.scalar(select(User))

    profile = SshProfile(
        owner_id=user.id,
        name="Box",
        host="127.0.0.1",
        port=1,
        username="nobody",
        host_key="ssh-ed25519 AAAA",
        host_fingerprint="SHA256:x",
        default_dir="/srv/project",
    )
    connection = Connection(name="c", base_url="http://127.0.0.1:1", api_key_encrypted="")
    db.add_all([profile, connection])
    db.commit()
    db.add(Model(connection_id=connection.id, model_id="m", capabilities_json={"tools": True}))
    db.commit()

    chat = Chat(
        user_id=user.id,
        model_id="m",
        connection_id=connection.id,
        kind=KIND_AGENT,
        ssh_profile_id=profile.id,
        project_dir="/srv/project",
        agent_mode="auto",
    )
    db.add(chat)
    db.commit()
    return chat


async def test_an_agent_reply_that_called_a_tool_renders(
    client: TestClient, db, agent_chat, registered, monkeypatch
):
    """The regression this file exists for: the bubble is rendered from the
    steps now, and an agent reply is where every kind of step appears at once."""
    db.add(Message(chat_id=agent_chat.id, role="user", content="Do the thing.", complete=True))
    reply = Message(chat_id=agent_chat.id, role="assistant", content="", complete=False)
    db.add(reply)
    db.commit()
    reply_id = reply.id

    monkeypatch.setattr(
        generation_service,
        "stream_chat",
        _stub_stream(
            [
                [
                    _text_chunk("Listing the project first. "),
                    _tool_chunk("file_list", _json.dumps({"path": "."})),
                ],
                [_text_chunk("Nothing I can reach.")],
            ]
        ),
    )
    monkeypatch.setattr("lembas.services.chat.generate_title", lambda *a, **k: "")

    generation = generation_service.Generation(chat_id=agent_chat.id, message_id=reply_id)
    await generation_service._run(generation)

    db.expire_all()
    stored = db.get(Message, reply_id)
    assert stored.tool_calls_json, "the tool event was recorded"
    assert stored.steps_json, "and so were the marks"

    page = client.get(f"/chat/{agent_chat.id}")
    assert page.status_code == 200
    assert "Listing the project first" in page.text
    assert "Nothing I can reach" in page.text
    assert page.text.index("Listing the project first") < page.text.index("tool-activity")


def test_the_page_renders_before_any_reply_exists(client: TestClient, agent_chat, registered):
    """An agent chat with nothing in it. The composer takes a different branch
    here -- the mode select, the jobs chip -- and it is the screen somebody sees
    first."""
    page = client.get(f"/chat/{agent_chat.id}")

    assert page.status_code == 200
    assert 'name="agent_mode"' in page.text


def test_starting_an_agent_chat_from_the_composer_renders(
    client: TestClient, db, registered, agent_chat
):
    """`POST /api/chats/start` returns the thread it just created. This is the
    exact moment the reader reported a blank screen."""
    profile = db.scalar(select(SshProfile))
    response = client.post(
        "/api/chats/start",
        data={
            "content": "Do the thing.",
            "kind": "agent",
            "ssh_profile_id": profile.id,
            "project_dir": "/srv/project",
            "model_id": "m",
        },
    )

    # 204 plus HX-Redirect is the contract: the row is created here and the
    # browser then navigates to it. The blank screen is on the page it lands on.
    assert response.status_code == 204
    assert response.headers["HX-Redirect"].startswith("/chat/")

    landed = client.get(response.headers["HX-Redirect"])
    assert landed.status_code == 200
    assert "Do the thing." in landed.text


def test_the_page_renders_while_the_reply_is_still_unfinished(
    client: TestClient, db, agent_chat, registered
):
    """The streaming branch of the bubble, which is what somebody sees for the
    whole of a long agent reply -- and what they saw blank.

    An unfinished assistant message is the *only* thing that starts a
    generation, so this is also the page that sets the reply going.
    """
    db.add(Message(chat_id=agent_chat.id, role="user", content="Do the thing.", complete=True))
    db.add(Message(chat_id=agent_chat.id, role="assistant", content="", complete=False))
    db.commit()

    page = client.get(f"/chat/{agent_chat.id}")

    assert page.status_code == 200
    assert "Do the thing." in page.text
    assert 'sse-swap="steps"' in page.text, "the container the frames land in"
    assert 'sse-swap="render"' in page.text


async def test_the_stream_delivers_the_steps_frame(
    client: TestClient, db, agent_chat, registered, monkeypatch
):
    """The SSE path with the new frame set, which is the one place the steps are
    rendered without a `Message` row to hang them on."""
    db.add(Message(chat_id=agent_chat.id, role="user", content="Do it.", complete=True))
    reply = Message(chat_id=agent_chat.id, role="assistant", content="", complete=False)
    db.add(reply)
    db.commit()
    reply_id = reply.id

    monkeypatch.setattr(
        generation_service,
        "stream_chat",
        _stub_stream(
            [
                [
                    _text_chunk("Listing first. "),
                    _tool_chunk("file_list", _json.dumps({"path": "."})),
                ],
                [_text_chunk("Done.")],
            ]
        ),
    )
    monkeypatch.setattr("lembas.services.chat.generate_title", lambda *a, **k: "")

    with client.stream(
        "GET", f"/api/chats/{agent_chat.id}/messages/{reply_id}/stream"
    ) as response:
        assert response.status_code == 200
        body = "".join(response.iter_lines())

    assert "event: steps" in body, "the closed steps never reached the browser"
    assert "event: done" in body
    assert "Listing first" in body
