"""Deterministic contracts for BESTIARY catalog resolution and filtering."""

from __future__ import annotations

import json
from datetime import datetime, timezone
from pathlib import Path

import pytest

from obliteratus import models_client


def _catalog(*records: dict) -> dict:
    return {"generated_at": "2026-08-16T00:00:00Z", "models": list(records)}


def _write_catalog(path: Path, payload: object) -> Path:
    path.write_text(json.dumps(payload), encoding="utf-8")
    return path


@pytest.fixture
def isolated_sources(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    monkeypatch.delenv("BESTIARY_CATALOG", raising=False)
    monkeypatch.setattr(models_client, "_LOCAL", tmp_path / "local.json")
    monkeypatch.setattr(models_client, "_CANONICAL", tmp_path / "canonical.json")
    monkeypatch.setattr(models_client, "_HERE", tmp_path / "package")


def test_explicit_path_is_authoritative_and_does_not_fall_back(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    isolated_sources: None,
) -> None:
    _write_catalog(models_client._LOCAL, _catalog({"id": "fallback"}))
    missing = tmp_path / "operator-selected.json"
    monkeypatch.setenv("BESTIARY_CATALOG", str(missing))

    with pytest.raises(FileNotFoundError, match="operator-selected"):
        models_client._load()


def test_default_resolution_uses_first_existing_packaged_source(
    isolated_sources: None,
) -> None:
    models_client._CANONICAL.parent.mkdir(parents=True, exist_ok=True)
    _write_catalog(models_client._CANONICAL, _catalog({"id": "canonical"}))
    state = models_client._HERE / "state" / "catalog.json"
    state.parent.mkdir(parents=True)
    _write_catalog(state, _catalog({"id": "state"}))

    assert models_client.model_ids() == ["canonical"]


def test_http_resolution_is_timeout_and_size_bounded(
    monkeypatch: pytest.MonkeyPatch,
    isolated_sources: None,
) -> None:
    payload = json.dumps(_catalog({"id": "remote"})).encode()
    calls: dict[str, object] = {}

    class Response:
        def __enter__(self):
            return self

        def __exit__(self, *_args):
            return False

        def read(self, amount: int) -> bytes:
            calls["read"] = amount
            return payload

    def urlopen(url: str, *, timeout: int):
        calls["url"] = url
        calls["timeout"] = timeout
        return Response()

    monkeypatch.setenv("BESTIARY_CATALOG", "https://catalog.invalid/catalog.json")
    monkeypatch.setattr(models_client.urllib.request, "urlopen", urlopen)

    assert models_client.model_ids() == ["remote"]
    assert calls == {
        "url": "https://catalog.invalid/catalog.json",
        "timeout": models_client._CATALOG_TIMEOUT_SECONDS,
        "read": models_client._MAX_CATALOG_BYTES + 1,
    }


def test_http_resolution_rejects_oversized_catalog(
    monkeypatch: pytest.MonkeyPatch,
    isolated_sources: None,
) -> None:
    class Response:
        def __enter__(self):
            return self

        def __exit__(self, *_args):
            return False

        def read(self, amount: int) -> bytes:
            return b"x" * amount

    monkeypatch.setenv("BESTIARY_CATALOG", "https://catalog.invalid/catalog.json")
    monkeypatch.setattr(
        models_client.urllib.request,
        "urlopen",
        lambda *_args, **_kwargs: Response(),
    )

    with pytest.raises(ValueError, match="size limit"):
        models_client._load()


@pytest.mark.parametrize("payload", [b"\xff", b"{not-json}"])
def test_path_resolution_rejects_invalid_utf8_or_json(
    payload: bytes,
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    isolated_sources: None,
) -> None:
    path = tmp_path / "catalog.json"
    path.write_bytes(payload)
    monkeypatch.setenv("BESTIARY_CATALOG", str(path))
    with pytest.raises(ValueError, match="not valid UTF-8 JSON"):
        models_client._load()


def test_path_resolution_is_size_bounded(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    isolated_sources: None,
) -> None:
    path = tmp_path / "catalog.json"
    path.write_bytes(b"123")
    monkeypatch.setattr(models_client, "_MAX_CATALOG_BYTES", 2)
    monkeypatch.setenv("BESTIARY_CATALOG", str(path))
    with pytest.raises(ValueError, match="size limit"):
        models_client._load()


def test_filters_reject_non_string_arguments(isolated_sources: None) -> None:
    with pytest.raises(TypeError, match="channel must be a string"):
        models_client.models(channel=7)  # type: ignore[arg-type]
    with pytest.raises(TypeError, match="open_weight must be a boolean"):
        models_client.models(open_weight=1)  # type: ignore[arg-type]


@pytest.mark.parametrize(
    "payload, message",
    [
        ([], "root must be an object"),
        ({}, "models must be a list"),
        ({"models": ["not-a-record"]}, "record 0 must be an object"),
    ],
)
def test_catalog_schema_fails_closed(
    payload: object,
    message: str,
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    isolated_sources: None,
) -> None:
    path = _write_catalog(tmp_path / "catalog.json", payload)
    monkeypatch.setenv("BESTIARY_CATALOG", str(path))

    with pytest.raises(ValueError, match=message) as error:
        models_client._load()
    assert str(error.value) == f"BESTIARY catalog {message}"


def test_filters_normalize_aliases_and_catalog_values(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    isolated_sources: None,
) -> None:
    path = _write_catalog(
        tmp_path / "catalog.json",
        _catalog(
            {
                "id": "Example/Alpha",
                "vendor": "Example",
                "channels": ["OpenRouter"],
                "open_weight": False,
                "capabilities": ["Tools"],
            },
            {
                "id": "Example/Beta",
                "vendor": "example",
                "channels": ["huggingface"],
                "open_weight": True,
                "capabilities": ["vision"],
            },
        ),
    )
    monkeypatch.setenv("BESTIARY_CATALOG", str(path))

    assert models_client.model_ids(channel="API", vendor="EXAMPLE", capability="TOOLS") == [
        "Example/Alpha"
    ]
    assert models_client.model_ids(channel="open-weight") == ["Example/Beta"]
    assert models_client.model_ids(channel="HF", open_weight=True) == ["Example/Beta"]


@pytest.mark.parametrize(
    "record, message",
    [
        ({"id": "bad", "channels": "api"}, "channels must be a list"),
        ({"id": "bad", "capabilities": [7]}, "capabilities must be a list"),
        ({"id": "bad", "vendor": 7}, "vendor must be a string"),
        ({"id": "bad", "open_weight": "false"}, "open_weight must be a boolean"),
    ],
)
def test_filters_reject_malformed_catalog_fields(
    record: dict,
    message: str,
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    isolated_sources: None,
) -> None:
    path = _write_catalog(tmp_path / "catalog.json", _catalog(record))
    monkeypatch.setenv("BESTIARY_CATALOG", str(path))

    with pytest.raises(ValueError, match=message):
        models_client.models()


@pytest.mark.parametrize("bad_id", [None, "", 7])
def test_model_ids_rejects_missing_or_non_string_ids(
    bad_id: object,
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    isolated_sources: None,
) -> None:
    path = _write_catalog(tmp_path / "catalog.json", _catalog({"id": bad_id}))
    monkeypatch.setenv("BESTIARY_CATALOG", str(path))

    with pytest.raises(ValueError, match="non-empty string id"):
        models_client.model_ids()


def test_newest_uses_injected_clock_release_fallback_and_stable_order(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    isolated_sources: None,
) -> None:
    path = _write_catalog(
        tmp_path / "catalog.json",
        _catalog(
            {"id": "released", "released": "2026-08-15"},
            {"id": "newest", "first_seen": "2026-08-16"},
            {"id": "cutoff", "first_seen": "2026-08-09"},
            {"id": "old", "first_seen": "2026-08-08"},
            {"id": "invalid", "first_seen": "yesterday"},
            {"id": "undated"},
        ),
    )
    monkeypatch.setenv("BESTIARY_CATALOG", str(path))
    monkeypatch.setattr(
        models_client,
        "_utc_now",
        lambda: datetime(2026, 8, 16, tzinfo=timezone.utc),
    )

    assert models_client.model_ids() == [
        "released", "newest", "cutoff", "old", "invalid", "undated"
    ]
    assert [record["id"] for record in models_client.newest(7)] == [
        "newest", "released", "cutoff"
    ]


def test_newest_rejects_non_string_catalog_dates(
    monkeypatch: pytest.MonkeyPatch,
    tmp_path: Path,
    isolated_sources: None,
) -> None:
    path = _write_catalog(
        tmp_path / "catalog.json",
        _catalog({"id": "bad-date", "first_seen": 20260816}),
    )
    monkeypatch.setenv("BESTIARY_CATALOG", str(path))
    with pytest.raises(ValueError, match="dates must be strings"):
        models_client.newest()


@pytest.mark.parametrize("days", [-1, True, 1.5, "7"])
def test_newest_rejects_invalid_day_windows(
    days: object,
    isolated_sources: None,
) -> None:
    with pytest.raises((TypeError, ValueError), match="days"):
        models_client.newest(days)  # type: ignore[arg-type]
