"""Unit tests for OllamaClient."""
from __future__ import annotations

import json
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from libs.common.retries import RetryableError
from libs.llm.client import OllamaClient
from libs.llm.exceptions import LLMError, LLMTimeoutError


@pytest.mark.unit
class TestOllamaClientChat:
    """Tests for OllamaClient.chat()."""

    @pytest.fixture
    def client(self) -> OllamaClient:
        return OllamaClient(base_url="http://localhost:11434", model="llama3.2")

    @pytest.mark.asyncio
    async def test_chat_success_returns_parsed_json(self, client: OllamaClient) -> None:
        """A successful Ollama response is parsed and returned as a dict."""
        expected = {"event_type": "earnings_release", "event_direction": "bullish"}
        sync_result = (expected, {"prompt_tokens": 100, "completion_tokens": 50}, 42)

        with patch.object(client, "_sync_call", return_value=sync_result):
            async with client:
                result, token_usage, elapsed_ms = await client.chat(
                    [{"role": "user", "content": "analyze this"}]
                )

        assert result == expected
        assert token_usage["prompt_tokens"] == 100
        assert token_usage["completion_tokens"] == 50
        assert elapsed_ms >= 0

    @pytest.mark.asyncio
    async def test_chat_timeout_raises_llm_timeout_error(self, client: OllamaClient) -> None:
        """Timeout raises LLMTimeoutError (which is also RetryableError)."""
        with patch.object(
            client,
            "_sync_call",
            side_effect=LLMTimeoutError("timeout", source="ollama", context={}),
        ):
            async with client:
                with pytest.raises(LLMTimeoutError):
                    await client.chat([{"role": "user", "content": "test"}])

    @pytest.mark.asyncio
    async def test_chat_5xx_raises_retryable_error(self, client: OllamaClient) -> None:
        """5xx response raises RetryableError."""
        with patch.object(
            client,
            "_sync_call",
            side_effect=RetryableError(
                "Ollama server error 503",
                source="ollama",
                context={"status": 503, "body": "Service Unavailable"},
            ),
        ):
            async with client:
                with pytest.raises(RetryableError):
                    await client.chat([{"role": "user", "content": "test"}])

    @pytest.mark.asyncio
    async def test_cache_hit_skips_llm_call(self) -> None:
        """LLMParser returns cached result without calling Ollama."""
        from unittest.mock import AsyncMock as AM

        from libs.llm.cache import LLMCacheStore
        from libs.llm.parser import LLMParser

        cached_output = {
            "schema_version": "1.0.0",
            "document_id": "DOC::test",
            "parser_kind": "llm",
            "event_type": "earnings_release",
            "event_direction": "bullish",
            "event_date": "2026-01-01",
            "filing_time_bucket": "post_market",
            "headline": "Test",
            "summary": "Test summary",
            "guidance": {"status": "raised", "scope": "annual", "notes": ""},
            "signals": {
                "demand_strength": "strong",
                "pricing_power": "present",
                "backlog_or_bookings": "present",
                "customer_expansion": "present",
                "margin_quality": "improving",
            },
            "risk_flags": {
                "oneoff_item": False,
                "tax_benefit": False,
                "valuation_gain": False,
                "non_gaap_heavy": False,
                "financing_related": False,
                "legal_or_regulatory_overhang": False,
            },
            "evidence": [],
            "confidence": {
                "overall": 0.85,
                "event_type": 0.9,
                "event_direction": 0.8,
                "guidance": 0.8,
                "risk_flags": 0.9,
            },
            "warnings": [],
        }

        mock_cache = MagicMock(spec=LLMCacheStore)
        mock_cache.get = AM(return_value=cached_output)

        mock_client = MagicMock(spec=OllamaClient)
        mock_client.model = "llama3.2"

        llm_parser = LLMParser(client=mock_client, cache_store=mock_cache)
        mock_session = AsyncMock()

        result = await llm_parser.parse(
            document_id="DOC::test",
            doc_text="some text",
            doc_meta={"filing_date": "2026-01-01", "form_type": "8-K"},
            rule_hints={},
            session=mock_session,
        )

        assert result == cached_output
        mock_client.chat.assert_not_called()
