"""POST /api/eid/chat mode=deep — ReAct 자동검색 SSE 변환 (ds-eid-ask-absorb P1).

★ DB·LLM 0: get_session/get_current_user dependency override, probe·agentic_ask_loop·
  get_backend monkeypatch. 실제 검색·27B 호출 없음.
★ 검증: 검색성→phase:searching+content+eid_sources+DONE / probe 실패→503 /
  mid-stream BackendUnavailable→in-stream error envelope / 대화성→sources 빈.
"""

from __future__ import annotations

import json
import sys
import types
from pathlib import Path

import pytest
import pytest_asyncio
from fastapi import FastAPI
from httpx import ASGITransport, AsyncClient

sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "app"))

import api.eid_chat as eid_chat  # noqa: E402
from api.eid_chat import router as eid_chat_router  # noqa: E402
from core.auth import get_current_user  # noqa: E402
from core.database import get_session  # noqa: E402
from services.llm.backends import BackendUnavailable  # noqa: E402
from services.search.react_loop import ReactResult  # noqa: E402

_DEEP = {"mode": "deep", "messages": [{"role": "user", "content": "콜드박스 위험성평가 찾아줘"}]}


async def _async_true() -> bool:
    return True


async def _async_false() -> bool:
    return False


def _build_app() -> FastAPI:
    app = FastAPI()
    app.include_router(eid_chat_router, prefix="/api/eid")
    app.dependency_overrides[get_current_user] = lambda: types.SimpleNamespace(
        id=1, username="test-user"
    )

    async def _fake_session():
        yield None  # deep 경로는 session 을 agentic_ask_loop 에 넘기기만(여기선 monkeypatch)

    app.dependency_overrides[get_session] = _fake_session
    return app


def _data_objs(raw: bytes) -> list[dict]:
    out: list[dict] = []
    for line in raw.split(b"\n"):
        if line.startswith(b"data: ") and line[len(b"data: "):].strip() != b"[DONE]":
            try:
                out.append(json.loads(line[len(b"data: "):]))
            except Exception:
                pass
    return out


@pytest_asyncio.fixture
async def client():
    async with AsyncClient(
        transport=ASGITransport(app=_build_app()), base_url="http://test"
    ) as ac:
        yield ac


@pytest.fixture(autouse=True)
def _rules_present(monkeypatch):
    # D-6 fail-closed 가드 통과 (substrate degraded 아님)
    monkeypatch.setattr(eid_chat.eid_compose, "rules_present", lambda: True)


@pytest.mark.asyncio
async def test_deep_search_sse_shape(client, monkeypatch):
    """검색성 질문 → phase:searching + final content + eid_sources + DONE 순서."""
    monkeypatch.setattr(eid_chat, "_probe_router_reachable", _async_true)
    monkeypatch.setattr(eid_chat, "get_backend", lambda name: object())

    async def _fake_loop(session, query, *, backend, **kw):
        return ReactResult(
            final_answer="콜드박스 위험성평가는 TK-RA-2026-OT1-01 입니다.",
            iterations=1,
            partial=False,
            sources=[{"id": 1, "doc_id": 10, "title": "OT1 콜드박스 위험성평가", "score": 0.91}],
        )

    monkeypatch.setattr(eid_chat, "agentic_ask_loop", _fake_loop)

    r = await client.post("/api/eid/chat", json=_DEEP)
    assert r.status_code == 200
    objs = _data_objs(r.content)
    assert "searching" in [o.get("phase") for o in objs if "phase" in o]
    content = "".join(
        o["choices"][0]["delta"]["content"] for o in objs if "choices" in o
    )
    assert "OT1-01" in content
    srcs = [o["eid_sources"] for o in objs if "eid_sources" in o]
    assert srcs and srcs[0][0]["title"] == "OT1 콜드박스 위험성평가"
    assert b"data: [DONE]" in r.content


@pytest.mark.asyncio
async def test_deep_conversational_no_sources(client, monkeypatch):
    """대화성(검색 불요) → ReAct early-exit, sources 빈 배열."""
    monkeypatch.setattr(eid_chat, "_probe_router_reachable", _async_true)
    monkeypatch.setattr(eid_chat, "get_backend", lambda name: object())

    async def _chat_loop(session, query, *, backend, **kw):
        return ReactResult(final_answer="안녕하세요, 이드입니다.", iterations=1, partial=False, sources=[])

    monkeypatch.setattr(eid_chat, "agentic_ask_loop", _chat_loop)

    r = await client.post("/api/eid/chat", json=_DEEP)
    assert r.status_code == 200
    objs = _data_objs(r.content)
    srcs = [o["eid_sources"] for o in objs if "eid_sources" in o]
    assert srcs and srcs[0] == []  # 검색 안 함 = 근거 카드 안 뜸


@pytest.mark.asyncio
async def test_deep_probe_fail_503(client, monkeypatch):
    """probe 실패(router 미도달) → 첫 바이트 전 503 router_unreachable."""
    monkeypatch.setattr(eid_chat, "_probe_router_reachable", _async_false)
    r = await client.post("/api/eid/chat", json=_DEEP)
    assert r.status_code == 503
    assert r.json()["error_reason"] == "router_unreachable"


@pytest.mark.asyncio
async def test_deep_midstream_error_envelope(client, monkeypatch):
    """검색 중 BackendUnavailable(AC 분리 등) → in-stream error envelope + DONE."""
    monkeypatch.setattr(eid_chat, "_probe_router_reachable", _async_true)
    monkeypatch.setattr(eid_chat, "get_backend", lambda name: object())

    async def _fail_loop(session, query, *, backend, **kw):
        raise BackendUnavailable("qwen-macbook", "macbook_unavailable")

    monkeypatch.setattr(eid_chat, "agentic_ask_loop", _fail_loop)

    r = await client.post("/api/eid/chat", json=_DEEP)
    assert r.status_code == 200  # 스트림 이미 시작(probe 통과) → 200 + in-stream error
    objs = _data_objs(r.content)
    errs = [o for o in objs if o.get("phase") == "error"]
    assert errs and errs[0]["error_reason"] == "macbook_unavailable"
    assert b"data: [DONE]" in r.content
