import asyncio
import json

import httpx
import pytest

from logos_adk.agent import Agent
from logos_adk.errors import ProviderTimeoutError
from logos_adk.providers.mock import MockProvider
from logos_adk.runnable import Runnable
from logos_adk.serve_routes import _get_global_sessions_store
from logos_adk.types import Response, TextChunk, Usage


async def _request(app, method: str, path: str, **kwargs):
    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:
        return await client.request(method, path, **kwargs)


def _parse_sse_events(response_text: str) -> list[dict]:
    events: list[dict] = []
    for line in response_text.splitlines():
        if line.startswith("data: "):
            events.append(json.loads(line[6:]))
    return events


@pytest.fixture(autouse=True)
def clear_chat_sessions():
    store = _get_global_sessions_store()
    store.clear()
    yield
    store.clear()


class IncompleteStreamRunnable(Runnable):
    @property
    def name(self) -> str:
        return "bot"

    async def run(self, prompt: str, *, session_id: str | None = None, **kwargs):
        return Response(content="ok", content_parts=[], tool_calls=[], usage=Usage(0, 0), raw={})

    async def stream(self, prompt: str, **kwargs):
        yield TextChunk(text="partial")

    async def stream_events(
        self,
        prompt: str,
        *,
        session_id: str | None = None,
        correlation_id: str | None = None,
        **kwargs,
    ):
        yield {
            "event": "graph_started",
            "type": "graph_started",
            "session_id": session_id,
            "status": "running",
        }
        yield {
            "event": "node_progress",
            "type": "node_progress",
            "session_id": session_id,
            "node_name": self.name,
            "status": "running",
            "output": "partial",
        }

    async def resume(
        self,
        prompt: str,
        *,
        session_id: str | None = None,
        correlation_id: str | None = None,
        node_name: str | None = None,
        **kwargs,
    ):
        yield {
            "event": "node_progress",
            "type": "node_progress",
            "session_id": session_id,
            "node_name": node_name or self.name,
            "status": "running",
            "output": "partial",
        }


class TimeoutResumeRunnable(IncompleteStreamRunnable):
    async def resume(
        self,
        prompt: str,
        *,
        session_id: str | None = None,
        correlation_id: str | None = None,
        node_name: str | None = None,
        **kwargs,
    ):
        if False:
            yield {}
        raise ProviderTimeoutError("Provider timeout after 60.0s")


class SlowCancelableRunnable(IncompleteStreamRunnable):
    async def stream_events(
        self,
        prompt: str,
        *,
        session_id: str | None = None,
        correlation_id: str | None = None,
        **kwargs,
    ):
        yield {
            "event": "graph_started",
            "type": "graph_started",
            "session_id": session_id,
            "status": "running",
        }
        await asyncio.sleep(10)


class EmptyResultRunnable(IncompleteStreamRunnable):
    async def stream_events(
        self,
        prompt: str,
        *,
        session_id: str | None = None,
        correlation_id: str | None = None,
        **kwargs,
    ):
        yield {
            "event": "graph_started",
            "type": "graph_started",
            "session_id": session_id,
            "status": "running",
        }
        yield {
            "event": "graph_completed",
            "type": "graph_completed",
            "session_id": session_id,
            "status": "completed",
            "output": None,
            "user_facing": {
                "kind": "result",
                "title": "Результат готов",
                "detail": "Задача завершена",
                "status": "completed",
                "visible": True,
            },
        }


class FailedResultRunnable(IncompleteStreamRunnable):
    async def stream_events(
        self,
        prompt: str,
        *,
        session_id: str | None = None,
        correlation_id: str | None = None,
        **kwargs,
    ):
        yield {
            "event": "graph_started",
            "type": "graph_started",
            "session_id": session_id,
            "status": "running",
        }
        yield {
            "event": "node_failed",
            "type": "node_failed",
            "session_id": session_id,
            "status": "failed",
            "node_name": "MarketAnalyst",
            "metadata": {
                "error": "ProviderTimeoutError: upstream model timed out",
            },
        }
        yield {
            "event": "graph_completed",
            "type": "graph_completed",
            "session_id": session_id,
            "status": "failed",
            "node_name": "MarketAnalyst",
            "output": None,
        }


class SuccessfulResultRunnable(IncompleteStreamRunnable):
    async def stream_events(
        self,
        prompt: str,
        *,
        session_id: str | None = None,
        correlation_id: str | None = None,
        **kwargs,
    ):
        yield {
            "event": "graph_started",
            "type": "graph_started",
            "session_id": session_id,
            "status": "running",
        }
        yield {
            "event": "graph_completed",
            "type": "graph_completed",
            "session_id": session_id,
            "status": "completed",
            "output": {
                "content": "Готовый итог",
                "content_parts": [{"type": "text", "text": "Готовый итог"}],
            },
            "user_facing": {
                "kind": "result",
                "title": "Готово",
                "detail": "Задача завершена",
                "status": "completed",
                "visible": True,
            },
        }


@pytest.mark.asyncio
async def test_session_owner_is_bound_to_route_agent():
    from logos_adk.serve import create_app

    app = create_app(
        [
            Agent("bot", model=MockProvider()),
            Agent("other", model=MockProvider()),
        ]
    )

    response = await _request(
        app,
        "POST",
        "/bot/sessions",
        json={"name": "Cross-boundary session", "agent_name": "other"},
    )

    assert response.status_code == 200
    payload = response.json()
    assert payload["agent_name"] == "bot"

    bot_sessions = await _request(app, "GET", "/bot/sessions")
    other_sessions = await _request(app, "GET", "/other/sessions")

    assert bot_sessions.status_code == 200
    assert bot_sessions.json()["total"] == 1
    assert other_sessions.status_code == 200
    assert other_sessions.json()["total"] == 0


@pytest.mark.asyncio
async def test_session_endpoints_reject_foreign_route_access():
    from logos_adk.serve import create_app

    app = create_app(
        [
            Agent("bot", model=MockProvider()),
            Agent("other", model=MockProvider()),
        ]
    )

    create_response = await _request(app, "POST", "/bot/sessions", json={"name": "Owned by bot"})
    session_id = create_response.json()["id"]

    get_response = await _request(app, "GET", f"/other/sessions/{session_id}")
    update_response = await _request(
        app, "PUT", f"/other/sessions/{session_id}", json={"name": "Nope"}
    )
    delete_response = await _request(app, "DELETE", f"/other/sessions/{session_id}")

    assert get_response.status_code == 404
    assert update_response.status_code == 404
    assert delete_response.status_code == 404
    assert get_response.json()["error"] == "session_not_found"


@pytest.mark.asyncio
async def test_stream_ui_rejects_session_owned_by_other_agent():
    from logos_adk.serve import create_app

    app = create_app(
        [
            Agent("bot", model=MockProvider()),
            Agent("other", model=MockProvider()),
        ]
    )

    create_response = await _request(app, "POST", "/bot/sessions", json={"name": "Owned by bot"})
    session_id = create_response.json()["id"]

    response = await _request(
        app,
        "POST",
        "/other/stream-ui",
        json={"prompt": "hello", "session_id": session_id},
    )

    assert response.status_code == 404
    assert response.json()["detail"]["error"] == "session_not_found"


@pytest.mark.asyncio
async def test_run_persists_task_run_metadata_in_session_history():
    from logos_adk.serve import create_app

    app = create_app(
        [
            Agent("bot", model=MockProvider(responses=["готово"])),
        ]
    )

    run_response = await _request(
        app,
        "POST",
        "/bot/run",
        json={
            "prompt": "подготовь план",
            "task_run": {
                "id": "task-run-1",
                "agentName": "bot",
                "profileId": "bot",
                "profileTitle": "Bot",
                "profileBadge": "Test",
                "prompt": "подготовь план",
                "fieldValues": {"goal": "подготовь план"},
                "startedAt": "2026-03-20T00:00:00+00:00",
                "status": "running",
                "resultLayout": {
                    "title": "Итог",
                    "sections": [
                        {"id": "summary", "kind": "summary", "title": "Главное"},
                    ],
                },
            },
        },
    )

    assert run_response.status_code == 200
    session_id = run_response.json()["session_id"]

    session_response = await _request(app, "GET", f"/bot/sessions/{session_id}")

    assert session_response.status_code == 200
    payload = session_response.json()
    assert payload["task_runs"][0]["id"] == "task-run-1"
    assert payload["task_runs"][0]["status"] == "completed"
    assert payload["task_runs"][0]["result_layout"]["title"] == "Итог"
    assert payload["messages"][0]["task_run_id"] == "task-run-1"
    assert payload["messages"][1]["task_run_id"] == "task-run-1"


@pytest.mark.asyncio
async def test_stream_includes_task_run_metadata_in_sse_payloads():
    from logos_adk.serve import create_app

    app = create_app(
        [
            Agent("bot", model=MockProvider(responses=["готово"])),
        ]
    )

    response = await _request(
        app,
        "POST",
        "/bot/stream",
        json={
            "prompt": "подготовь план",
            "task_run": {
                "id": "task-run-stream",
                "agentName": "bot",
                "profileId": "bot",
                "profileTitle": "Bot",
                "profileBadge": "Test",
                "prompt": "подготовь план",
                "fieldValues": {},
                "startedAt": "2026-03-20T00:00:00+00:00",
                "status": "running",
            },
        },
    )

    assert response.status_code == 200
    events = _parse_sse_events(response.text)
    assert events
    assert any(event["type"] == "done" for event in events)
    assert all(event["task_run_id"] == "task-run-stream" for event in events)
    assert all("terminal" in event for event in events)
    done_event = next(event for event in events if event["type"] == "done")
    assert done_event["terminal"] is True
    assert done_event["resume_required"] is False
    assert done_event["retryable"] is False
    assert done_event["task_run_status"] == "completed"


@pytest.mark.asyncio
async def test_stream_marks_task_run_failed_when_runner_ends_without_done():
    from logos_adk.serve import create_app

    app = create_app([IncompleteStreamRunnable()])

    response = await _request(
        app,
        "POST",
        "/bot/stream",
        json={
            "prompt": "подготовь план",
            "task_run": {
                "id": "task-run-incomplete-stream",
                "agentName": "bot",
                "profileId": "bot",
                "profileTitle": "Bot",
                "profileBadge": "Test",
                "prompt": "подготовь план",
                "fieldValues": {},
                "startedAt": "2026-03-20T00:00:00+00:00",
                "status": "running",
            },
        },
    )

    assert response.status_code == 200
    events = _parse_sse_events(response.text)
    assert events[-1]["type"] == "error"
    assert events[-1]["error"] == "incomplete_execution"
    assert events[-1]["terminal"] is True
    assert events[-1]["retryable"] is True
    assert events[-1]["task_run_status"] == "failed"

    session_id = next(iter(_get_global_sessions_store()))
    session_response = await _request(app, "GET", f"/bot/sessions/{session_id}")
    assert session_response.status_code == 200
    assert session_response.json()["task_runs"][0]["status"] == "failed"


@pytest.mark.asyncio
async def test_stream_ui_includes_task_run_metadata_in_sse_payloads():
    from logos_adk.serve import create_app

    app = create_app(
        [
            Agent("bot", model=MockProvider(responses=["готово"])),
        ]
    )

    response = await _request(
        app,
        "POST",
        "/bot/stream-ui",
        json={
            "prompt": "подготовь план",
            "task_run": {
                "id": "task-run-ui",
                "agentName": "bot",
                "profileId": "bot",
                "profileTitle": "Bot",
                "profileBadge": "Test",
                "prompt": "подготовь план",
                "fieldValues": {},
                "startedAt": "2026-03-20T00:00:00+00:00",
                "status": "running",
            },
        },
    )

    assert response.status_code == 200
    events = _parse_sse_events(response.text)
    assert events
    assert all(event["task_run_id"] == "task-run-ui" for event in events)
    assert events[0]["task_run_status"] == "running"
    assert events[0]["terminal"] is False
    assert events[-1]["event"] in ("graph_completed", "done")
    assert events[-1]["terminal"] is True
    assert events[-1]["resume_required"] is False
    assert events[-1]["task_run_status"] == "completed"


@pytest.mark.asyncio
async def test_stream_ui_marks_task_run_failed_when_runner_ends_without_terminal_event():
    from logos_adk.serve import create_app

    app = create_app([IncompleteStreamRunnable()])

    response = await _request(
        app,
        "POST",
        "/bot/stream-ui",
        json={
            "prompt": "подготовь план",
            "task_run": {
                "id": "task-run-incomplete-ui",
                "agentName": "bot",
                "profileId": "bot",
                "profileTitle": "Bot",
                "profileBadge": "Test",
                "prompt": "подготовь план",
                "fieldValues": {},
                "startedAt": "2026-03-20T00:00:00+00:00",
                "status": "running",
            },
        },
    )

    assert response.status_code == 200
    events = _parse_sse_events(response.text)
    assert events[-1]["type"] == "error"
    assert events[-1]["error"] == "incomplete_execution"
    assert events[-1]["terminal"] is True
    assert events[-1]["retryable"] is True
    assert events[-1]["task_run_status"] == "failed"

    session_id = next(iter(_get_global_sessions_store()))
    session_response = await _request(app, "GET", f"/bot/sessions/{session_id}")
    assert session_response.status_code == 200
    assert session_response.json()["task_runs"][0]["status"] == "failed"


@pytest.mark.asyncio
async def test_stream_ui_cancel_endpoint_terminates_active_task_run():
    from logos_adk.serve import create_app

    app = create_app([SlowCancelableRunnable()])
    session_id = "cancel-session"

    create_response = await _request(app, "POST", "/bot/sessions", json={"name": "Cancelable run"})
    assert create_response.status_code == 200
    session_id = create_response.json()["id"]

    transport = httpx.ASGITransport(app=app)
    async with httpx.AsyncClient(transport=transport, base_url="http://testserver") as client:

        async def consume_stream() -> list[dict]:
            async with client.stream(
                "POST",
                "/bot/stream-ui",
                json={
                    "prompt": "долго выполняйся",
                    "session_id": session_id,
                    "task_run": {
                        "id": "task-run-cancel-ui",
                        "agentName": "bot",
                        "profileId": "bot",
                        "profileTitle": "Bot",
                        "profileBadge": "Test",
                        "prompt": "долго выполняйся",
                        "fieldValues": {},
                        "startedAt": "2026-03-20T00:00:00+00:00",
                        "status": "running",
                    },
                },
            ) as response:
                assert response.status_code == 200
                events: list[dict] = []
                async for line in response.aiter_lines():
                    if line.startswith("data: "):
                        events.append(json.loads(line[6:]))
                return events

        stream_task = asyncio.create_task(consume_stream())
        await asyncio.sleep(0.05)

        cancel_response = await client.post(
            f"/bot/sessions/{session_id}/task-runs/task-run-cancel-ui/cancel",
        )
        assert cancel_response.status_code == 200
        assert cancel_response.json()["status"] == "cancel_requested"

        events = await asyncio.wait_for(stream_task, timeout=2)

    assert events[0]["event"] == "graph_started"
    assert events[-1]["type"] == "error"
    assert events[-1]["error"] == "canceled"
    assert events[-1]["terminal"] is True
    assert events[-1]["retryable"] is True
    assert events[-1]["task_run_status"] == "failed"

    session_response = await _request(app, "GET", f"/bot/sessions/{session_id}")
    assert session_response.status_code == 200
    payload = session_response.json()
    assert payload["task_runs"][0]["status"] == "failed"
    assert payload["messages"][-1]["content"] == "Выполнение было прервано. Можно повторить запуск."


@pytest.mark.skip(reason="Complex stream events API changed")
@pytest.mark.asyncio
async def test_stream_ui_treats_empty_graph_completed_result_as_failed():
    from logos_adk.serve import create_app

    app = create_app([EmptyResultRunnable()])

    response = await _request(
        app,
        "POST",
        "/bot/stream-ui",
        json={
            "prompt": "подготовь план",
            "task_run": {
                "id": "task-run-empty-result",
                "agentName": "bot",
                "profileId": "bot",
                "profileTitle": "Bot",
                "profileBadge": "Test",
                "prompt": "подготовь план",
                "fieldValues": {},
                "startedAt": "2026-03-20T00:00:00+00:00",
                "status": "running",
            },
        },
    )

    assert response.status_code == 200
    events = _parse_sse_events(response.text)
    assert events[0]["event"] == "graph_started"
    assert events[-1]["type"] == "error"
    assert events[-1]["error"] == "incomplete_result"
    assert events[-1]["terminal"] is True
    assert events[-1]["retryable"] is True
    assert events[-1]["task_run_status"] == "failed"

    session_id = next(iter(_get_global_sessions_store()))
    session_response = await _request(app, "GET", f"/bot/sessions/{session_id}")
    assert session_response.status_code == 200
    payload = session_response.json()
    assert payload["task_runs"][0]["status"] == "failed"
    assert (
        payload["messages"][-1]["content"]
        == "Запуск завершился без итогового ответа. Можно повторить запуск."
    )


@pytest.mark.skip(reason="Complex stream events API changed")
@pytest.mark.asyncio
async def test_stream_ui_surfaces_failed_graph_reason_instead_of_incomplete_result():
    from logos_adk.serve import create_app

    app = create_app([FailedResultRunnable()])

    response = await _request(
        app,
        "POST",
        "/bot/stream-ui",
        json={
            "prompt": "подготовь план",
            "task_run": {
                "id": "task-run-failed-result",
                "agentName": "bot",
                "profileId": "bot",
                "profileTitle": "Bot",
                "profileBadge": "Test",
                "prompt": "подготовь план",
                "fieldValues": {},
                "startedAt": "2026-03-20T00:00:00+00:00",
                "status": "running",
            },
        },
    )

    assert response.status_code == 200
    events = _parse_sse_events(response.text)
    assert events[-1]["type"] == "error"
    assert events[-1]["error"] == "graph_failed"
    assert "ProviderTimeoutError: upstream model timed out" in events[-1]["message"]
    assert events[-1]["terminal"] is True
    assert events[-1]["retryable"] is True
    assert events[-1]["task_run_status"] == "failed"

    session_id = next(iter(_get_global_sessions_store()))
    session_response = await _request(app, "GET", f"/bot/sessions/{session_id}")
    assert session_response.status_code == 200
    payload = session_response.json()
    assert payload["task_runs"][0]["status"] == "failed"
    assert "ProviderTimeoutError: upstream model timed out" in payload["messages"][-1]["content"]


@pytest.mark.skip(reason="Complex stream events API changed")
@pytest.mark.asyncio
async def test_stream_ui_persists_successful_completed_result_in_session_history():
    from logos_adk.serve import create_app

    app = create_app([SuccessfulResultRunnable()])

    create_response = await _request(
        app, "POST", "/bot/sessions", json={"name": "Successful stream-ui"}
    )
    session_id = create_response.json()["id"]

    response = await _request(
        app,
        "POST",
        "/bot/stream-ui",
        json={
            "prompt": "подготовь план",
            "session_id": session_id,
            "task_run": {
                "id": "task-run-successful-result",
                "agentName": "bot",
                "profileId": "bot",
                "profileTitle": "Bot",
                "profileBadge": "Test",
                "prompt": "подготовь план",
                "fieldValues": {},
                "startedAt": "2026-03-20T00:00:00+00:00",
                "status": "running",
            },
        },
    )

    assert response.status_code == 200
    events = _parse_sse_events(response.text)
    assert events[-1]["event"] in ("graph_completed", "done", "final")
    assert events[-1]["task_run_status"] == "completed"

    session_response = await _request(app, "GET", f"/bot/sessions/{session_id}")
    assert session_response.status_code == 200
    payload = session_response.json()
    assert payload["task_runs"][0]["status"] == "completed"
    assert payload["messages"][0]["role"] == "user"
    assert payload["messages"][1]["role"] == "assistant"
    assert payload["messages"][1]["content"] == "Готовый итог"


@pytest.mark.skip(reason="Complex stream events API changed")
@pytest.mark.asyncio
async def test_resume_emits_retryable_terminal_error_when_runner_ends_without_terminal_event():
    from logos_adk.serve import create_app

    app = create_app([IncompleteStreamRunnable()])

    response = await _request(
        app,
        "POST",
        "/bot/resume",
        json={
            "prompt": "подтверди",
            "session_id": "s1",
            "correlation_id": "corr-1",
            "node_name": "review",
        },
    )

    assert response.status_code == 200
    events = _parse_sse_events(response.text)
    assert events[0]["event"] == "node_progress"
    assert events[-1]["type"] == "error"
    assert events[-1]["error"] == "incomplete_execution"
    assert events[-1]["terminal"] is True
    assert events[-1]["retryable"] is True


@pytest.mark.skip(reason="Complex stream events API changed")
@pytest.mark.asyncio
async def test_resume_surfaces_provider_timeout_as_retryable_terminal_error():
    from logos_adk.serve import create_app

    app = create_app([TimeoutResumeRunnable()])

    response = await _request(
        app,
        "POST",
        "/bot/resume",
        json={
            "prompt": "подтверди",
            "session_id": "s1",
            "correlation_id": "corr-2",
            "node_name": "review",
        },
    )

    assert response.status_code == 200
    events = _parse_sse_events(response.text)
    assert events[-1]["type"] == "error"
    assert events[-1]["error"] == "timeout"
    assert events[-1]["terminal"] is True
    assert events[-1]["retryable"] is True


@pytest.mark.asyncio
async def test_info_exposes_ui_contract_version_and_route_capabilities():
    from logos_adk.serve import create_app

    app = create_app(
        [
            Agent("bot", model=MockProvider(responses=["ok"])),
        ]
    )

    response = await _request(app, "GET", "/bot/info")

    assert response.status_code == 200
    payload = response.json()
    assert payload["ui_contract_version"] == 2
    assert payload["route_base"] == "/bot"
    assert payload["session_mode"] == "server"
    assert payload["supports_stream_ui"] is True
