"""
Testy `models_catalog.fetch_chat_models` — filtrowanie i cache.

Mockujemy httpx.AsyncClient.get tak, by zwracał ustaloną listę. Jeden
test sprawdza efekt cache (drugi call nie powinien dotykać HTTP).
"""

from __future__ import annotations

import os
import sys
import unittest
from unittest.mock import AsyncMock, patch

sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", ".."))

from mcp_nostr_explorer import models_catalog  # noqa: E402


def _ppq_item(
    id_: str,
    owned_by: str = "OpenAI",
    popular: bool = False,
    output_modalities=("text",),
    supported=("tools",),
    pricing=None,
) -> dict:
    return {
        "id": id_,
        "name": id_,
        "owned_by": owned_by,
        "popular": popular,
        "type": "chat",
        "architecture": {"output_modalities": list(output_modalities)},
        "supported_parameters": list(supported),
        "pricing": pricing or {
            "input_per_1M_tokens": 1.0,
            "output_per_1M_tokens": 4.0,
            "currency": "USD",
        },
    }


class _FakeResponse:
    def __init__(self, data):
        self._data = data
    def raise_for_status(self): pass
    def json(self): return self._data


class _FakeClient:
    def __init__(self, data):
        self.data = data
        self.calls = 0
    async def __aenter__(self): return self
    async def __aexit__(self, *a): return False
    async def get(self, url):
        self.calls += 1
        return _FakeResponse(self.data)


class TestFetchModels(unittest.IsolatedAsyncioTestCase):

    async def asyncSetUp(self):
        models_catalog.reset_cache()

    async def asyncTearDown(self):
        models_catalog.reset_cache()

    async def test_filtr_brak_tools_odrzuca(self):
        body = {"data": [
            _ppq_item("a", supported=()),                 # brak tools
            _ppq_item("b", supported=("tools",)),         # OK
        ]}
        fake = _FakeClient(body)
        with patch("mcp_nostr_explorer.models_catalog.httpx.AsyncClient", return_value=fake):
            out = await models_catalog.fetch_chat_models()
        ids = [m["id"] for m in out]
        self.assertEqual(ids, ["b"])

    async def test_filtr_brak_text_output_odrzuca(self):
        body = {"data": [
            _ppq_item("img-only", output_modalities=("image",)),
            _ppq_item("text-ok"),
        ]}
        fake = _FakeClient(body)
        with patch("mcp_nostr_explorer.models_catalog.httpx.AsyncClient", return_value=fake):
            out = await models_catalog.fetch_chat_models()
        ids = [m["id"] for m in out]
        self.assertEqual(ids, ["text-ok"])

    async def test_sortowanie_popular_pierwsze_potem_owned_by(self):
        body = {"data": [
            _ppq_item("z", owned_by="OpenAI", popular=False),
            _ppq_item("popular-anthropic", owned_by="Anthropic", popular=True),
            _ppq_item("popular-openai",     owned_by="OpenAI",    popular=True),
            _ppq_item("a", owned_by="Anthropic", popular=False),
        ]}
        fake = _FakeClient(body)
        with patch("mcp_nostr_explorer.models_catalog.httpx.AsyncClient", return_value=fake):
            out = await models_catalog.fetch_chat_models()
        ids = [m["id"] for m in out]
        # popularne pierwsze (Anthropic alfabetycznie przed OpenAI), potem niepopularne
        self.assertEqual(ids, [
            "popular-anthropic", "popular-openai",   # popular bucket
            "a", "z",                                # rest, owned_by-asc
        ])

    async def test_pricing_zredukowane(self):
        body = {"data": [_ppq_item("x", pricing={
            "type": "per_token",
            "currency": "USD",
            "input_per_1M_tokens": 5.5,
            "output_per_1M_tokens": 22,
        })]}
        fake = _FakeClient(body)
        with patch("mcp_nostr_explorer.models_catalog.httpx.AsyncClient", return_value=fake):
            out = await models_catalog.fetch_chat_models()
        self.assertEqual(out[0]["pricing"], {
            "input_per_1M": 5.5,
            "output_per_1M": 22,
            "currency": "USD",
        })

    async def test_kanonizacja_id_anthropic_kropki_na_myslniki(self):
        # PPQ /v1/models zwraca Anthropic-id z kropkami (claude-opus-4.7),
        # adapter Anthropic SDK używa myślników (claude-opus-4-7) — id w
        # zwróconej liście musi być w formacie myślnikowym.
        body = {"data": [
            _ppq_item("claude-opus-4.7", owned_by="Anthropic"),
            _ppq_item("claude-haiku-4.5", owned_by="Anthropic"),
            _ppq_item("~anthropic/claude-haiku-latest", owned_by="~anthropic"),
            _ppq_item("gpt-5.5-pro", owned_by="OpenAI"),
            _ppq_item("qwen/qwen3.5-plus-20260420", owned_by="Qwen"),
        ]}
        fake = _FakeClient(body)
        with patch("mcp_nostr_explorer.models_catalog.httpx.AsyncClient", return_value=fake):
            out = await models_catalog.fetch_chat_models()
        by_id = {m["id"]: m for m in out}
        # Anthropic — kropki zamienione na myślniki
        self.assertIn("claude-opus-4-7", by_id)
        self.assertIn("claude-haiku-4-5", by_id)
        # ~anthropic floating alias też leci na myślniki
        self.assertIn("~anthropic/claude-haiku-latest", by_id)
        # Nie-Anthropic zostaje bez zmian — kropki, ukośniki, daty
        self.assertIn("gpt-5.5-pro", by_id)
        self.assertIn("qwen/qwen3.5-plus-20260420", by_id)

    async def test_cache_drugie_wywolanie_nie_pyta_http(self):
        body = {"data": [_ppq_item("only")]}
        fake = _FakeClient(body)
        with patch("mcp_nostr_explorer.models_catalog.httpx.AsyncClient", return_value=fake):
            await models_catalog.fetch_chat_models()
            await models_catalog.fetch_chat_models()
            await models_catalog.fetch_chat_models()
        self.assertEqual(fake.calls, 1)


if __name__ == "__main__":
    unittest.main()
