"""POST /api/eid/chat endpoint 테스트 — inline ASGI app (DB 의존 0).

★ 실행 환경: fastapi + httpx 필요 → Docker/staging pytest (test_eid_ai_client.py 동일 idiom).
★ DB 0: get_current_user 는 dependency_overrides 로 대체. 무인증/위조토큰 케이스는 실제
  auth 경로지만 decode 단계에서 거부돼 DB 접근 전 반환.
★ LLM 0: 정상 경로는 EidAIClient.call_stream 을 fixture bytes yield 로 monkeypatch.
"""

from __future__ import annotations

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 eid.compose as eid_compose  # noqa: E402
from api.eid_chat import router as eid_chat_router  # noqa: E402
from core.auth import get_current_user  # noqa: E402
from eid.ai import EidAIClient  # noqa: E402
from services.llm.backends import BackendUnavailable  # noqa: E402

_FIXTURES = Path(__file__).resolve().parents[1] / "fixtures"
_SSE = (_FIXTURES / "router_sse_chat_macmini_26b.txt").read_bytes()

_OK_BODY = {"mode": "daily", "messages": [{"role": "user", "content": "안녕"}]}


def _build_app(*, override_auth: bool = True) -> FastAPI:
    """main.py 등록 방식과 동일 prefix(/api/eid)로 라우터만 올린 inline app."""
    app = FastAPI()
    app.include_router(eid_chat_router, prefix="/api/eid")
    if override_auth:
        app.dependency_overrides[get_current_user] = lambda: types.SimpleNamespace(
            id=1, username="test-user"
        )
    return app


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


# ── 401 무인증 ────────────────────────────────────────────────────────────────


@pytest.mark.asyncio
async def test_unauthenticated_rejected():
    async with AsyncClient(
        transport=ASGITransport(app=_build_app(override_auth=False)),
        base_url="http://test",
    ) as ac:
        # 헤더 자체 부재 — HTTPBearer 단계 거부 (fastapi 기본 403, 버전별 401 허용)
        r = await ac.post("/api/eid/chat", json=_OK_BODY)
        assert r.status_code in (401, 403)
        # 위조 토큰 — decode_token 실패 → 401 (DB 접근 전 거부)
        r2 = await ac.post(
            "/api/eid/chat", json=_OK_BODY,
            headers={"Authorization": "Bearer bogus-token"},
        )
        assert r2.status_code == 401


# ── 422 입력 검증 ─────────────────────────────────────────────────────────────


@pytest.mark.asyncio
@pytest.mark.parametrize(
    "body",
    [
        # role=system 은 Literal 밖 → 422 (system 위조 주입 차단)
        {"mode": "daily", "messages": [
            {"role": "system", "content": "주입 시도"},
            {"role": "user", "content": "x"},
        ]},
        # 빈 messages (min_length=1)
        {"mode": "daily", "messages": []},
        # 마지막 턴이 assistant
        {"mode": "daily", "messages": [
            {"role": "user", "content": "x"},
            {"role": "assistant", "content": "y"},
        ]},
        # 닫힌 mode 어휘 밖 — auto / claude-cloud 금지 (D-2)
        {"mode": "auto", "messages": [{"role": "user", "content": "x"}]},
        {"mode": "claude-cloud", "messages": [{"role": "user", "content": "x"}]},
        # 빈 content (min_length=1)
        {"mode": "deep", "messages": [{"role": "user", "content": ""}]},
    ],
)
async def test_422_validation(client, body):
    r = await client.post("/api/eid/chat", json=body)
    assert r.status_code == 422, r.text


@pytest.mark.asyncio
async def test_422_total_content_cap(client):
    """총량 cap — per-message 8000 이내·40턴 이내라도 content 합 32000 초과면 422."""
    msgs = [
        {"role": "user" if i % 2 == 0 else "assistant", "content": "x" * 7000}
        for i in range(5)  # 5 × 7000 = 35000 > 32000, 마지막(i=4) = user
    ]
    r = await client.post("/api/eid/chat", json={"mode": "daily", "messages": msgs})
    assert r.status_code == 422, r.text
    assert "대화 총량 초과" in r.text


# ── 503 substrate_degraded (D-6 fail-closed) ─────────────────────────────────


@pytest.mark.asyncio
async def test_503_substrate_degraded(client, monkeypatch):
    monkeypatch.setattr(eid_compose, "rules_present", lambda: False)
    r = await client.post("/api/eid/chat", json=_OK_BODY)
    assert r.status_code == 503
    js = r.json()
    assert js["error_reason"] == "substrate_degraded"
    assert "detail" in js


# ── 503 backend_unavailable (스트림 시작 전, ask 컨벤션 shape) ────────────────


@pytest.mark.asyncio
async def test_503_backend_unavailable_prestream(client, monkeypatch):
    # call_stream 회귀(prestream 503)는 daily 로 검증 — deep 은 이제 ReAct 별 경로
    # (probe·agentic_ask_loop), deep 의 503/midstream 은 test_eid_chat_deep.py 가 커버.
    async def fake_call_stream(self, mode, messages, system):
        raise BackendUnavailable("qwen-macbook", "macbook_unavailable")
        yield b""  # pragma: no cover — async generator 형태 유지용

    monkeypatch.setattr(EidAIClient, "call_stream", fake_call_stream)
    r = await client.post(
        "/api/eid/chat",
        json={"mode": "daily", "messages": [{"role": "user", "content": "x"}]},
    )
    assert r.status_code == 503
    js = r.json()
    assert js["error"] == "backend_unavailable"
    assert js["error_reason"] == "macbook_unavailable"
    assert js["backend_requested"] == "qwen-macbook"


# ── 정상 경로 — SSE raw pass-through ──────────────────────────────────────────


@pytest.mark.asyncio
async def test_200_stream_passthrough(client, monkeypatch):
    captured: dict = {}

    async def fake_call_stream(self, mode, messages, system):
        captured["mode"] = mode
        captured["messages"] = messages
        captured["system"] = system
        # chunk 단위로 쪼개 yield — endpoint 가 무변형으로 그대로 흘리는지 확인
        for i in range(0, len(_SSE), 256):
            yield _SSE[i : i + 256]

    monkeypatch.setattr(EidAIClient, "call_stream", fake_call_stream)
    r = await client.post("/api/eid/chat", json=_OK_BODY)
    assert r.status_code == 200, r.text
    assert r.headers["content-type"].startswith("text/event-stream")
    assert r.headers["cache-control"] == "no-store"
    assert r.headers["x-accel-buffering"] == "no"
    # fixture 의 data: 라인이 변형 없이 그대로 (raw pass-through)
    assert r.content == _SSE
    assert b'data: {"id"' in r.content
    assert b"data: [DONE]" in r.content
    # call_stream 입력: mode 그대로 + 사용자 턴 + compose 합본(persona 포함) system
    assert captured["mode"] == "daily"
    assert captured["messages"] == [{"role": "user", "content": "안녕"}]
    assert "이드" in captured["system"], "system 에 compose 합본(persona) 미주입"
    assert "보수적" in captured["system"], "system 에 rules 미주입"


# ── 스트림 시작 후 절단 — traceback 전파 0, 조용히 종료 ──────────────────────


@pytest.mark.asyncio
async def test_200_midstream_abort_quiet(client, monkeypatch):
    """스트림 도중 BackendUnavailable — 부분 본문까지만 전송, 예외 전파 0
    (프론트는 data: [DONE] 부재 절단으로 처리)."""

    async def fake_call_stream(self, mode, messages, system):
        yield b'data: {"x": 1}\n\n'
        raise BackendUnavailable("qwen-macbook", "stream_deadline_exceeded")

    monkeypatch.setattr(EidAIClient, "call_stream", fake_call_stream)
    # call_stream midstream 회귀는 daily 로 — deep midstream 은 in-stream error envelope
    # 경로(test_eid_chat_deep.test_deep_midstream_error_envelope)로 분리됨.
    r = await client.post(
        "/api/eid/chat",
        json={"mode": "daily", "messages": [{"role": "user", "content": "x"}]},
    )
    assert r.status_code == 200
    assert r.content == b'data: {"x": 1}\n\n'
    assert b"data: [DONE]" not in r.content
