import json
import os
from pathlib import Path

import httpx
from fastapi.testclient import TestClient

import app
import config
import db
import routers.completions
from security import SESSIONS, PIN_ATTEMPTS, RATE_EVENTS


def make_client(tmp_path: Path) -> TestClient:
    os.environ["CAIC_ADMIN_PIN"] = "1234"
    db.DB_PATH = tmp_path / "caic-completions.db"
    SESSIONS.clear()
    PIN_ATTEMPTS.clear()
    RATE_EVENTS.clear()
    db.init_db()
    return TestClient(app.app, raise_server_exceptions=False)


TEST_API_KEY = "test-sk-caic-completions"


def _auth_headers(extra: dict = None) -> dict:
    h = {"Authorization": f"Bearer {TEST_API_KEY}", "Content-Type": "application/json", "Origin": "http://testserver"}
    if extra:
        h.update(extra)
    return h


class _MockStreamResponse:
    def __init__(self, lines: list[str]):
        self._lines = lines

    async def __aenter__(self):
        return self

    async def __aexit__(self, exc_type, exc, tb):
        return False

    async def aiter_lines(self):
        for line in self._lines:
            yield line


class _MockAsyncPostResponse:
    def __init__(self, status_code=200, json_data=None):
        self.status_code = status_code
        self._json_data = json_data or {}

    def json(self):
        return self._json_data


def _stream_json_lines(events: list[dict]) -> list[str]:
    return [json.dumps(event) for event in events]


def test_completions_missing_api_key(tmp_path: Path):
    with make_client(tmp_path) as client:
        resp = client.post(
            "/v1/chat/completions",
            json={"messages": [{"role": "user", "content": "hi"}]},
        )
        assert resp.status_code == 401


def test_completions_invalid_api_key(tmp_path: Path):
    with make_client(tmp_path) as client:
        resp = client.post(
            "/v1/chat/completions",
            json={"messages": [{"role": "user", "content": "hi"}]},
            headers={"Authorization": "Bearer wrong-key", "Origin": "http://testserver"},
        )
        assert resp.status_code == 401


def test_completions_no_messages(tmp_path: Path, monkeypatch):
    monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
    with make_client(tmp_path) as client:
        resp = client.post("/v1/chat/completions", json={}, headers=_auth_headers())
        assert resp.status_code == 400


def test_completions_empty_messages(tmp_path: Path, monkeypatch):
    monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
    with make_client(tmp_path) as client:
        resp = client.post("/v1/chat/completions", json={"messages": []}, headers=_auth_headers())
        assert resp.status_code == 400


def test_completions_no_user_message(tmp_path: Path, monkeypatch):
    monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
    with make_client(tmp_path) as client:
        resp = client.post(
            "/v1/chat/completions",
            json={"messages": [{"role": "assistant", "content": "hello"}]},
            headers=_auth_headers(),
        )
        assert resp.status_code == 400


def test_completions_streaming(tmp_path: Path, monkeypatch):
    monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
    events = _stream_json_lines([
        {"choices": [{"delta": {"content": "Hello"}, "logprobs": None}]},
        {"choices": [{"delta": {"content": " world"}, "logprobs": None}]},
        {"choices": [{"delta": {}, "finish_reason": "stop"}], "usage": {"tokens_per_second": 15.0}},
    ])

    call_count = 0

    def stream_stub(self, method, url, json=None, timeout=None):
        nonlocal call_count
        call_count += 1
        return _MockStreamResponse(events)

    monkeypatch.setattr(httpx.AsyncClient, "stream", stream_stub)

    with make_client(tmp_path) as client:
        resp = client.post(
            "/v1/chat/completions",
            json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
            headers=_auth_headers(),
        )
        assert resp.status_code == 200
        body = resp.text
        assert "data: [DONE]" in body
        assert "Hello" in body or "world" in body
        assert "chatcmpl-" in body


def test_completions_blocking(tmp_path: Path, monkeypatch):
    monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
    events = _stream_json_lines([
        {"choices": [{"delta": {"content": "Hello world"}, "logprobs": None}]},
        {"choices": [{"delta": {}, "finish_reason": "stop"}], "usage": {}},
    ])

    def stream_stub(self, method, url, json=None, timeout=None):
        return _MockStreamResponse(events)

    monkeypatch.setattr(httpx.AsyncClient, "stream", stream_stub)

    with make_client(tmp_path) as client:
        resp = client.post(
            "/v1/chat/completions",
            json={"messages": [{"role": "user", "content": "hi"}], "stream": False},
            headers=_auth_headers(),
        )
        assert resp.status_code == 200
        data = resp.json()
        assert data["object"] == "chat.completion"
        assert data["choices"][0]["message"]["content"] == "Hello world"


def test_completions_fim_passthrough(tmp_path: Path, monkeypatch):
    monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
    fim_data = {"prompt": "def foo():\n    ", "suffix": "\n    return x", "model": "llama3.1:latest"}

    async def mock_post(self, url, json=None, timeout=None):
        return _MockAsyncPostResponse(json_data={"choices": [{"text": "pass"}], "usage": {}})

    monkeypatch.setattr(httpx.AsyncClient, "post", mock_post)

    with make_client(tmp_path) as client:
        resp = client.post("/v1/chat/completions", json=fim_data, headers=_auth_headers())
        assert resp.status_code == 200
        assert "choices" in resp.json()


def test_completions_connect_error_stream(tmp_path: Path, monkeypatch):
    monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)

    def broken_stream(self, method, url, json=None, timeout=None):
        raise httpx.ConnectError("Connection refused")

    monkeypatch.setattr(httpx.AsyncClient, "stream", broken_stream)

    with make_client(tmp_path) as client:
        resp = client.post(
            "/v1/chat/completions",
            json={"messages": [{"role": "user", "content": "hi"}], "stream": True},
            headers=_auth_headers(),
        )
        assert resp.status_code == 200
        assert "connection_error" in resp.text


def test_completions_connect_error_blocking(tmp_path: Path, monkeypatch):
    monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)

    def broken_stream(self, method, url, json=None, timeout=None):
        raise httpx.ConnectError("Connection refused")

    monkeypatch.setattr(httpx.AsyncClient, "stream", broken_stream)

    with make_client(tmp_path) as client:
        resp = client.post(
            "/v1/chat/completions",
            json={"messages": [{"role": "user", "content": "hi"}], "stream": False},
            headers=_auth_headers(),
        )
        assert resp.status_code == 503


def test_completions_fim_connect_error(tmp_path: Path, monkeypatch):
    monkeypatch.setattr(routers.completions, "COMPLETIONS_API_KEY", TEST_API_KEY)
    fim_data = {"prompt": "def foo():", "model": "llama3.1:latest"}

    def broken_post(self, url, json=None, timeout=None):
        raise httpx.ConnectError("Connection refused")

    monkeypatch.setattr(httpx.AsyncClient, "post", broken_post)

    with make_client(tmp_path) as client:
        resp = client.post("/v1/chat/completions", json=fim_data, headers=_auth_headers())
        assert resp.status_code == 503
