"""Tests for anima.core.model_registry.

Covers: embedded registry loading, user cache loading, context limit lookup
(exact match, provider prefix, prefix match, unknown model, empty registry),
internal JSON parser, and ModelInfo dataclass.
"""

from __future__ import annotations

import json
from pathlib import Path

import pytest

from anima.core.model_registry import (
    ModelInfo,
    _parse_registry_json,
    get_context_limit,
    load_embedded_registry,
    load_user_cache,
)
from anima.shell.model_config import DEFAULT_MODEL_CONTEXT_LIMIT_TOKENS


# ---------------------------------------------------------------------------
# 1. load_embedded_registry
# ---------------------------------------------------------------------------


def test_load_embedded_registry_returns_dict() -> None:
    """Embedded registry loads as a non-empty dict of ModelInfo objects."""
    registry = load_embedded_registry()

    assert isinstance(registry, dict)
    assert len(registry) > 0
    for key, value in list(registry.items())[:3]:
        assert isinstance(key, str)
        assert isinstance(value, ModelInfo)


def test_load_embedded_registry_has_key_models() -> None:
    """Embedded registry contains well-known models used by the project."""
    registry = load_embedded_registry()

    for model_id in ("gpt-5.2", "gpt-4o", "claude-sonnet-4-20250514"):
        assert model_id in registry, f"{model_id} missing from embedded registry"
        info = registry[model_id]
        assert info.context_window > 0
        assert info.model_id == model_id


# ---------------------------------------------------------------------------
# 2. get_context_limit
# ---------------------------------------------------------------------------


def test_get_context_limit_exact_match() -> None:
    """Exact model_id key returns the corresponding context window."""
    registry = {
        "gpt-5.2": ModelInfo("gpt-5.2", 272000, 128000),
        "gpt-4o": ModelInfo("gpt-4o", 128000, 16384),
    }

    assert get_context_limit("gpt-5.2", registry) == 272000
    assert get_context_limit("gpt-4o", registry) == 128000


def test_get_context_limit_provider_prefix() -> None:
    """Provider-prefixed model id (e.g. 'openai:gpt-5.2') resolves after stripping."""
    registry = {
        "gpt-5.2": ModelInfo("gpt-5.2", 272000, 128000),
    }

    assert get_context_limit("openai:gpt-5.2", registry) == 272000


def test_get_context_limit_prefix_match() -> None:
    """Longest-prefix matching resolves dated model variants."""
    registry = {
        "claude-sonnet-4": ModelInfo("claude-sonnet-4", 200000, 8192),
    }

    # "claude-sonnet-4-20250514" starts with "claude-sonnet-4"
    assert get_context_limit("claude-sonnet-4-20250514", registry) == 200000


def test_get_context_limit_unknown_model() -> None:
    """Unknown model falls back to DEFAULT_MODEL_CONTEXT_LIMIT_TOKENS."""
    registry = {
        "gpt-4o": ModelInfo("gpt-4o", 128000, 16384),
    }

    assert get_context_limit("totally-unknown-model", registry) == DEFAULT_MODEL_CONTEXT_LIMIT_TOKENS


def test_get_context_limit_empty_registry() -> None:
    """Empty registry always returns default."""
    assert get_context_limit("gpt-4o", {}) == DEFAULT_MODEL_CONTEXT_LIMIT_TOKENS


def test_get_context_limit_none_registry() -> None:
    """None registry returns default (matches the @pre/@post contract)."""
    assert get_context_limit("gpt-4o", None) == DEFAULT_MODEL_CONTEXT_LIMIT_TOKENS


# ---------------------------------------------------------------------------
# 3. load_user_cache
# ---------------------------------------------------------------------------


def test_load_user_cache_missing(tmp_path: Path) -> None:
    """Returns None when the cache directory has no cache file."""
    result = load_user_cache(tmp_path)
    assert result is None


def test_load_user_cache_valid(tmp_path: Path) -> None:
    """Loads a valid JSON cache file into a dict of ModelInfo."""
    cache_data = {
        "test-model": {"context_window": 64000, "max_output_tokens": 4096},
        "other-model": {"context_window": 32000, "max_output_tokens": None},
    }
    cache_file = tmp_path / "model_context_windows.json"
    cache_file.write_text(json.dumps(cache_data))

    result = load_user_cache(tmp_path)

    assert result is not None
    assert "test-model" in result
    assert result["test-model"].context_window == 64000
    assert result["test-model"].max_output_tokens == 4096
    assert "other-model" in result
    assert result["other-model"].context_window == 32000


def test_load_user_cache_invalid_json(tmp_path: Path) -> None:
    """Returns None when the cache file contains invalid JSON."""
    cache_file = tmp_path / "model_context_windows.json"
    cache_file.write_text("{not valid json!!!")

    result = load_user_cache(tmp_path)
    assert result is None


# ---------------------------------------------------------------------------
# 4. _parse_registry_json
# ---------------------------------------------------------------------------


def test_parse_registry_json() -> None:
    """Parses a sample registry dict into ModelInfo objects."""
    raw = {
        "model-a": {"context_window": 100000, "max_output_tokens": 8192},
        "model-b": {"context_window": 50000, "max_output_tokens": None},
        "bad-entry": {"context_window": -1},  # invalid, should be skipped
        "non-dict": "garbage",  # should be skipped
        "missing-cw": {"max_output_tokens": 1024},  # missing context_window
    }

    result = _parse_registry_json(raw)

    assert len(result) == 2
    assert result["model-a"] == ModelInfo("model-a", 100000, 8192)
    assert result["model-b"] == ModelInfo("model-b", 50000, None)


def test_parse_registry_json_non_dict_input() -> None:
    """Non-dict input returns empty dict."""
    assert _parse_registry_json("not a dict") == {}
    assert _parse_registry_json([1, 2, 3]) == {}
    assert _parse_registry_json(None) == {}


# ---------------------------------------------------------------------------
# 5. ModelInfo dataclass
# ---------------------------------------------------------------------------


def test_model_info_dataclass() -> None:
    """ModelInfo is a frozen dataclass with expected fields."""
    info = ModelInfo(model_id="test-model", context_window=128000, max_output_tokens=16384)

    assert info.model_id == "test-model"
    assert info.context_window == 128000
    assert info.max_output_tokens == 16384


def test_model_info_defaults() -> None:
    """max_output_tokens defaults to None."""
    info = ModelInfo(model_id="minimal", context_window=64000)

    assert info.max_output_tokens is None


def test_model_info_frozen() -> None:
    """ModelInfo is immutable (frozen=True)."""
    info = ModelInfo(model_id="test", context_window=128000)

    with pytest.raises(AttributeError):
        info.context_window = 999  # type: ignore[misc]
