"""Tests for apps.core.ai.client.

Covers:
* deterministic fallback when OPENAI_API_KEY is unset.
* deterministic fallback when the LLM call raises.
* successful LLM path when responses are mocked.
* ``summarise`` fallback + happy path.
"""
from __future__ import annotations

import json
from types import SimpleNamespace
from unittest import mock

import pytest

from apps.core.ai import client as ai_client


@pytest.fixture(autouse=True)
def _clear_openai_key(settings):
    """Stub mode by default so tests do not call the network."""
    settings.OPENAI_API_KEY = ""
    yield


def _profile():
    return {"sectors": ["agritech"], "geo": ["UG"], "stage": "growth", "stages": ["growth"]}


def _candidates():
    return [
        {"id": 1, "label": "A", "kind": "investor", "sectors": ["agritech"], "geo": ["UG"], "stages": ["growth"]},
        {"id": 2, "label": "B", "kind": "investor", "sectors": ["fintech"], "geo": ["KE"], "stages": ["seed"]},
        {"id": 3, "label": "C", "kind": "investor", "sectors": ["agritech"], "geo": ["TZ"], "stages": ["growth"]},
    ]


def test_recommend_uses_deterministic_when_no_key():
    out = ai_client.recommend(_profile(), _candidates(), top_k=3)
    assert len(out) == 3
    assert all(r["is_fallback"] is True for r in out)
    # Top candidate should be the agritech+UG+growth match
    assert out[0]["id"] == 1


def test_recommend_empty_candidates():
    assert ai_client.recommend(_profile(), [], top_k=5) == []


def test_recommend_uses_llm_when_key_present(settings):
    settings.OPENAI_API_KEY = "sk-test"
    fake_response = {
        "items": [
            {"id": "1", "score": 0.42, "reason": "weak"},
            {"id": "2", "score": 0.91, "reason": "strong sector"},
            {"id": "3", "score": 0.55, "reason": "moderate"},
        ]
    }
    fake_invoke = mock.Mock(
        return_value=SimpleNamespace(content=json.dumps(fake_response))
    )
    with mock.patch.object(ai_client, "_chat_fast", return_value=SimpleNamespace(invoke=fake_invoke)):
        out = ai_client.recommend(_profile(), _candidates(), top_k=3)
    assert all(r["is_fallback"] is False for r in out)
    # LLM said id=2 is strongest, so it must be first despite weaker deterministic features
    assert out[0]["id"] == 2
    assert out[0]["reason"] == "strong sector"


def test_recommend_falls_back_when_llm_raises(settings):
    settings.OPENAI_API_KEY = "sk-test"
    fake_invoke = mock.Mock(side_effect=RuntimeError("network down"))
    with mock.patch.object(ai_client, "_chat_fast", return_value=SimpleNamespace(invoke=fake_invoke)):
        out = ai_client.recommend(_profile(), _candidates(), top_k=3)
    assert all(r["is_fallback"] is True for r in out)
    # deterministic top still wins
    assert out[0]["id"] == 1


def test_recommend_handles_code_fenced_json(settings):
    settings.OPENAI_API_KEY = "sk-test"
    payload = {"items": [{"id": "1", "score": 0.7, "reason": "ok"}, {"id": "2", "score": 0.1, "reason": "weak"}, {"id": "3", "score": 0.5, "reason": "ok"}]}
    fake_content = "```json\n" + json.dumps(payload) + "\n```"
    fake_invoke = mock.Mock(return_value=SimpleNamespace(content=fake_content))
    with mock.patch.object(ai_client, "_chat_fast", return_value=SimpleNamespace(invoke=fake_invoke)):
        out = ai_client.recommend(_profile(), _candidates(), top_k=3)
    assert all(r["is_fallback"] is False for r in out)
    assert out[0]["id"] == 1


def test_summarise_returns_clipped_when_no_key():
    text = "x" * 500
    out = ai_client.summarise(text, max_chars=100)
    assert len(out) <= 101  # may include trailing ellipsis char
    assert out.endswith("…")


def test_summarise_uses_llm_when_key_present(settings):
    settings.OPENAI_API_KEY = "sk-test"
    fake_invoke = mock.Mock(return_value=SimpleNamespace(content="A succinct summary."))
    with mock.patch.object(ai_client, "_chat_fast", return_value=SimpleNamespace(invoke=fake_invoke)):
        out = ai_client.summarise("Some long text " * 50, max_chars=120)
    assert out == "A succinct summary."


def test_summarise_falls_back_on_error(settings):
    settings.OPENAI_API_KEY = "sk-test"
    fake_invoke = mock.Mock(side_effect=RuntimeError("boom"))
    with mock.patch.object(ai_client, "_chat_fast", return_value=SimpleNamespace(invoke=fake_invoke)):
        out = ai_client.summarise("hello world", max_chars=50)
    assert out == "hello world"
