import asyncio
import importlib
import json
import sys
from pathlib import Path

import pytest


BACKEND_DIR = Path(__file__).resolve().parents[1]
if str(BACKEND_DIR) not in sys.path:
    sys.path.insert(0, str(BACKEND_DIR))

try:
    llm = importlib.import_module("llm")
except ModuleNotFoundError:
    pytest.skip("llm module dependencies are not available", allow_module_level=True)


def test_extract_message_openai_format():
    resp = {"choices": [{"message": {"content": "hello world", "thinking": "reasoning"}}]}
    content, thinking = llm._extract_message(resp)
    assert content == "hello world"
    assert thinking == "reasoning"


def test_extract_message_openai_reasoning_content():
    resp = {"choices": [{"message": {"content": "answer", "reasoning_content": "deep thought"}}]}
    content, thinking = llm._extract_message(resp)
    assert content == "answer"
    assert thinking == "deep thought"


def test_extract_message_empty_choices():
    resp = {"choices": []}
    content, thinking = llm._extract_message(resp)
    assert content == ""
    assert thinking == ""


def test_extract_message_no_choices_key():
    resp = {}
    content, thinking = llm._extract_message(resp)
    assert content == ""
    assert thinking == ""


def test_extract_message_none_content():
    resp = {"choices": [{"message": {"content": None, "thinking": None}}]}
    content, thinking = llm._extract_message(resp)
    assert content == ""
    assert thinking == ""


def test_extract_delta_text():
    chunk = {"choices": [{"delta": {"content": "hello"}}]}
    assert llm._extract_delta_text(chunk) == "hello"


def test_extract_delta_text_empty():
    chunk = {"choices": [{"delta": {}}]}
    assert llm._extract_delta_text(chunk) == ""


def test_extract_delta_thinking():
    chunk = {"choices": [{"delta": {"thinking": "reasoning step"}}]}
    assert llm._extract_delta_thinking(chunk) == "reasoning step"


def test_extract_delta_reasoning_content():
    chunk = {"choices": [{"delta": {"reasoning_content": "deep thought"}}]}
    assert llm._extract_delta_thinking(chunk) == "deep thought"


def test_resolve_model_name_explicit():
    assert llm._resolve_model_name("custom-model") == "custom-model"


def test_resolve_model_name_default():
    assert llm._resolve_model_name() == llm.LLM_MODEL


def test_resolve_model_name_pro():
    assert llm._resolve_model_name(use_pro_model=True) == llm.PRO_LLM_MODEL


def test_resolve_system_prompt():
    assert llm._resolve_system_prompt("  system prompt  ") == "system prompt"
    assert llm._resolve_system_prompt("") == ""
    assert llm._resolve_system_prompt(None) == ""


def test_build_chat_payload_with_system():
    payload = llm._build_chat_payload(
        "user prompt", system_prompt="sys prompt", temperature=0.5, model="test-model"
    )
    assert payload["model"] == "test-model"
    assert len(payload["messages"]) == 2
    assert payload["messages"][0]["role"] == "system"
    assert payload["messages"][1]["role"] == "user"
    assert payload["stream"] is False


def test_build_chat_payload_no_system():
    payload = llm._build_chat_payload("user prompt", system_prompt=None)
    assert len(payload["messages"]) == 1
    assert payload["stream"] is False


def test_build_chat_payload_with_thinking():
    payload = llm._build_chat_payload("prompt", thinking="low")
    assert "options" in payload
    assert payload["options"]["think"] == "low"


def test_build_chat_stream_payload():
    payload = llm._build_chat_stream_payload("prompt", system_prompt="sys")
    assert payload["stream"] is True
    assert len(payload["messages"]) == 2


def test_build_chat_stream_payload_with_thinking():
    payload = llm._build_chat_stream_payload("prompt", thinking="high")
    assert "options" in payload
    assert payload["options"]["think"] == "high"


def test_call_ollama_non_streaming(monkeypatch):
    captured = {}

    async def fake_post(*args, **kwargs):
        captured["url"] = args[1] if len(args) > 1 else kwargs.get("url", "")
        captured["json"] = kwargs.get("json")

        class FakeResp:
            def raise_for_status(self): pass
            def json(self): return {"choices": [{"message": {"content": "done"}}]}

        return FakeResp()

    def fake_client(*args, **kwargs):
        class Ctx:
            async def __aenter__(self2): return self2
            async def __aexit__(*a): pass
            post = fake_post
        return Ctx()

    monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client)
    monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro)

    result = asyncio.run(
        llm.call_ollama("test prompt", system_prompt="sys", tag="t1")
    )

    assert result["content"] == "done"
    assert captured["url"] == "/chat/completions"
    assert captured["json"]["stream"] is False


def test_stream_ollama_text_deltas(monkeypatch):
    captured = {}

    def make_lines():
        lines_iter = iter([
            'data: {"choices": [{"delta": {"content": "hel"}}]}',
            'data: {"choices": [{"delta": {"content": "lo"}}]}',
            "data: [DONE]",
        ])

        class LineIterator:
            def __aiter__(self2): return self2
            async def __anext__(self2):
                try:
                    return next(lines_iter)
                except StopIteration:
                    raise StopAsyncIteration()

        class Response:
            def __init__(self2): self2._lines = LineIterator()

            def raise_for_status(self2): pass
            def aiter_lines(self2): return self2._lines

        class StreamCtx:
            async def __aenter__(self2): return Response()
            async def __aexit__(*a): pass

        class Client:
            stream = lambda self2, *args, **kw: StreamCtx()
            async def __aenter__(self2): return self2
            async def __aexit__(*a): pass

        return Client()

    def fake_client(*args, **kwargs):
        captured["called"] = True
        return make_lines()

    monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client)
    monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro)

    results = []
    async def collect():
        async for delta in llm.stream_ollama("prompt", tag="t1"):
            results.append(delta)

    asyncio.run(collect())
    assert captured.get("called") is True
    assert results == ["hel", "lo"]


def test_stream_ollama_events_thinking_and_content(monkeypatch):
    captured = {}

    def make_lines():
        lines_iter = iter([
            'data: {"choices": [{"delta": {"thinking": "reasoning"}}]}',
            'data: {"choices": [{"delta": {"content": "answer"}}]}',
            "data: [DONE]",
        ])

        class LineIterator:
            def __aiter__(self2): return self2
            async def __anext__(self2):
                try:
                    return next(lines_iter)
                except StopIteration:
                    raise StopAsyncIteration()

        class Response:
            def __init__(self2): self2._lines = LineIterator()

            def raise_for_status(self2): pass
            def aiter_lines(self2): return self2._lines

        class StreamCtx:
            async def __aenter__(self2): return Response()
            async def __aexit__(*a): pass

        class Client:
            stream = lambda self2, *args, **kw: StreamCtx()
            async def __aenter__(self2): return self2
            async def __aexit__(*a): pass

        return Client()

    def fake_client(*args, **kwargs):
        captured["called"] = True
        return make_lines()

    monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client)
    monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro)

    results = []
    async def collect():
        async for event_type, payload in llm.stream_ollama_events("prompt", tag="t1"):
            results.append((event_type, payload))

    asyncio.run(collect())
    assert captured.get("called") is True
    # First event should be thinking, then content
    assert results[0] == ("thinking", "")
    assert results[1][0] == "content"


def test_call_vlm_ocr(monkeypatch):
    captured = {}

    async def fake_post(*args, **kwargs):
        captured["url"] = args[1] if len(args) > 1 else kwargs.get("url", "")
        captured["json"] = kwargs.get("json")

        class FakeResp:
            def raise_for_status(self): pass
            def json(self): return {"choices": [{"message": {"content": "ocr text"}}]}

        return FakeResp()

    def fake_client(*args, **kwargs):
        class Ctx:
            async def __aenter__(self2): return self2
            async def __aexit__(*a): pass
            post = fake_post
        return Ctx()

    monkeypatch.setattr(llm.httpx, "AsyncClient", fake_client)
    monkeypatch.setattr(llm.asyncio, "wait_for", lambda coro, **kw: coro)

    result = asyncio.run(llm.call_vlm_ocr(b"fake image bytes"))
    assert result == "ocr text"

    # Verify the payload uses OpenAI vision format (image_url)
    assert captured["url"] == "/chat/completions"
    messages = captured["json"]["messages"]
    assert len(messages) == 1
    content_parts = messages[0]["content"]
    # Should have text part and image_url part
    assert any(p.get("type") == "text" for p in content_parts)
    image_part = [p for p in content_parts if p.get("type") == "image_url"]
    assert len(image_part) == 1
    assert image_part[0]["image_url"]["url"].startswith("data:image/png;base64,")
    assert captured["json"]["options"]["think"] is False
