"""Tests that generated _generated/api/*_base.py files have correct structure."""

from __future__ import annotations

import pytest

from goodmem._base import AsyncCRUDNamespace, CRUDNamespace


# ---------------------------------------------------------------------------
# CRUD bases
# ---------------------------------------------------------------------------

class TestEmbeddersBase:
    def test_inherits_crud_namespace(self):
        from goodmem._generated.api.embedders_base import EmbeddersAPIBase, AsyncEmbeddersAPIBase
        assert issubclass(EmbeddersAPIBase, CRUDNamespace)
        assert issubclass(AsyncEmbeddersAPIBase, AsyncCRUDNamespace)

    def test_base_path(self):
        from goodmem._generated.api.embedders_base import EmbeddersAPIBase
        assert EmbeddersAPIBase._base_path == "/v1/embedders"

    def test_model_attributes(self):
        from goodmem._generated.api.embedders_base import EmbeddersAPIBase
        from goodmem._generated.models.embedder_creation_request import EmbedderCreationRequest
        from goodmem._generated.models.update_embedder_request import UpdateEmbedderRequest
        from goodmem._generated.models.embedder_response import EmbedderResponse
        from goodmem._generated.models.list_embedders_response import ListEmbeddersResponse
        assert EmbeddersAPIBase._create_model is EmbedderCreationRequest
        assert EmbeddersAPIBase._update_model is UpdateEmbedderRequest
        assert EmbeddersAPIBase._response_model is EmbedderResponse
        assert EmbeddersAPIBase._list_response_model is ListEmbeddersResponse
        assert EmbeddersAPIBase._list_items_field == "embedders"


class TestRerankersBase:
    def test_inherits_crud_namespace(self):
        from goodmem._generated.api.rerankers_base import RerankersAPIBase, AsyncRerankersAPIBase
        assert issubclass(RerankersAPIBase, CRUDNamespace)
        assert issubclass(AsyncRerankersAPIBase, AsyncCRUDNamespace)

    def test_base_path(self):
        from goodmem._generated.api.rerankers_base import RerankersAPIBase
        assert RerankersAPIBase._base_path == "/v1/rerankers"

    def test_list_items_field(self):
        from goodmem._generated.api.rerankers_base import RerankersAPIBase
        assert RerankersAPIBase._list_items_field == "rerankers"


class TestSpacesBase:
    def test_inherits_crud_namespace(self):
        from goodmem._generated.api.spaces_base import SpacesAPIBase, AsyncSpacesAPIBase
        assert issubclass(SpacesAPIBase, CRUDNamespace)
        assert issubclass(AsyncSpacesAPIBase, AsyncCRUDNamespace)

    def test_base_path(self):
        from goodmem._generated.api.spaces_base import SpacesAPIBase
        assert SpacesAPIBase._base_path == "/v1/spaces"

    def test_list_items_field(self):
        from goodmem._generated.api.spaces_base import SpacesAPIBase
        assert SpacesAPIBase._list_items_field == "spaces"


class TestApikeysBase:
    def test_inherits_crud_namespace(self):
        from goodmem._generated.api.apikeys_base import ApikeysAPIBase, AsyncApikeysAPIBase
        assert issubclass(ApikeysAPIBase, CRUDNamespace)
        assert issubclass(AsyncApikeysAPIBase, AsyncCRUDNamespace)

    def test_base_path(self):
        from goodmem._generated.api.apikeys_base import ApikeysAPIBase
        assert ApikeysAPIBase._base_path == "/v1/apikeys"

    def test_list_items_field(self):
        from goodmem._generated.api.apikeys_base import ApikeysAPIBase
        assert ApikeysAPIBase._list_items_field == "keys"

    def test_no_get_method(self):
        """ApiKeys has no GET-by-ID endpoint; get() must not exist."""
        from goodmem._generated.api.apikeys_base import ApikeysAPIBase
        assert not hasattr(ApikeysAPIBase, "get")


class TestLlmsBase:
    def test_inherits_crud_namespace(self):
        from goodmem._generated.api.llms_base import LlmsAPIBase, AsyncLlmsAPIBase
        assert issubclass(LlmsAPIBase, CRUDNamespace)
        assert issubclass(AsyncLlmsAPIBase, AsyncCRUDNamespace)

    def test_base_path(self):
        from goodmem._generated.api.llms_base import LlmsAPIBase
        assert LlmsAPIBase._base_path == "/v1/llms"

    def test_list_items_field(self):
        from goodmem._generated.api.llms_base import LlmsAPIBase
        assert LlmsAPIBase._list_items_field == "llms"

    def test_create_method_exists(self):
        """LLMs base overrides create() to unwrap the response envelope."""
        from goodmem._generated.api.llms_base import LlmsAPIBase
        assert hasattr(LlmsAPIBase, "create")
        # The base defines its own create, not inherited from CRUDNamespace
        assert "create" in LlmsAPIBase.__dict__

    def test_response_model_is_llm_response(self):
        from goodmem._generated.api.llms_base import LlmsAPIBase
        from goodmem._generated.models.llm_response import LLMResponse
        assert LlmsAPIBase._response_model is LLMResponse


# ---------------------------------------------------------------------------
# Memories base — has extra methods
# ---------------------------------------------------------------------------

class TestMemoriesBase:
    def test_inherits_crud_namespace(self):
        from goodmem._generated.api.memories_base import MemoriesAPIBase, AsyncMemoriesAPIBase
        assert issubclass(MemoriesAPIBase, CRUDNamespace)
        assert issubclass(AsyncMemoriesAPIBase, AsyncCRUDNamespace)

    def test_base_path(self):
        from goodmem._generated.api.memories_base import MemoriesAPIBase
        assert MemoriesAPIBase._base_path == "/v1/memories"

    def test_list_items_field(self):
        from goodmem._generated.api.memories_base import MemoriesAPIBase
        assert MemoriesAPIBase._list_items_field == "memories"

    def test_has_retrieve_with_body_helper(self):
        from goodmem._generated.api.memories_base import MemoriesAPIBase
        assert hasattr(MemoriesAPIBase, "_retrieve_with_body")

    def test_has_retrieve_method(self):
        from goodmem._generated.api.memories_base import MemoriesAPIBase
        assert hasattr(MemoriesAPIBase, "retrieve")

    def test_has_batch_methods(self):
        from goodmem._generated.api.memories_base import MemoriesAPIBase
        assert hasattr(MemoriesAPIBase, "batch_create")
        assert hasattr(MemoriesAPIBase, "batch_get")
        assert hasattr(MemoriesAPIBase, "batch_delete")

    def test_has_content(self):
        from goodmem._generated.api.memories_base import MemoriesAPIBase
        assert hasattr(MemoriesAPIBase, "content")

    def test_has_space_scoped_list(self):
        from goodmem._generated.api.memories_base import MemoriesAPIBase
        import inspect
        sig = inspect.signature(MemoriesAPIBase.list)
        assert "space_id" in sig.parameters


# ---------------------------------------------------------------------------
# Non-CRUD bases
# ---------------------------------------------------------------------------

class TestUsersBase:
    def test_importable(self):
        from goodmem._generated.api.users_base import UsersAPIBase, AsyncUsersAPIBase
        assert UsersAPIBase is not None
        assert AsyncUsersAPIBase is not None

    def test_has_expected_methods(self):
        from goodmem._generated.api.users_base import UsersAPIBase
        assert hasattr(UsersAPIBase, "me")
        assert hasattr(UsersAPIBase, "get")

    def test_not_crud_namespace(self):
        """Users is read-only; it should NOT inherit from CRUDNamespace."""
        from goodmem._generated.api.users_base import UsersAPIBase
        assert not issubclass(UsersAPIBase, CRUDNamespace)

    def test_get_rejects_both_id_and_email(self):
        import httpx
        from goodmem._generated.api.users_base import UsersAPIBase
        api = UsersAPIBase(httpx.Client(), {})
        with pytest.raises(ValueError, match="Exactly one of"):
            api.get(id="abc", email="x@example.com")
        api._http.close()

    def test_get_rejects_neither_id_nor_email(self):
        import httpx
        from goodmem._generated.api.users_base import UsersAPIBase
        api = UsersAPIBase(httpx.Client(), {})
        with pytest.raises(ValueError, match="Exactly one of"):
            api.get()
        api._http.close()


class TestOcrBase:
    def test_has_document_method(self):
        from goodmem._generated.api.ocr_base import OcrAPIBase
        assert hasattr(OcrAPIBase, "document")

    def test_not_crud_namespace(self):
        from goodmem._generated.api.ocr_base import OcrAPIBase
        assert not issubclass(OcrAPIBase, CRUDNamespace)


class TestSystemBase:
    def test_has_expected_methods(self):
        from goodmem._generated.api.system_base import SystemAPIBase
        assert hasattr(SystemAPIBase, "info")
        assert hasattr(SystemAPIBase, "init")


class TestAdminBase:
    def test_has_expected_methods(self):
        from goodmem._generated.api.admin_base import AdminAPIBase
        assert hasattr(AdminAPIBase, "drain")

    def test_has_sub_namespaces(self):
        import httpx
        from goodmem._generated.api.admin_base import AdminAPIBase
        inst = AdminAPIBase(httpx.Client(), {})
        assert hasattr(inst, "background_jobs")
        assert hasattr(inst.background_jobs, "purge")
        assert hasattr(inst, "license")
        assert hasattr(inst.license, "reload")
        inst._http.close()


# ---------------------------------------------------------------------------
# _rest_metadata completeness & correctness
# ---------------------------------------------------------------------------

# (module_path, class_name, expected_method_keys)
_ALL_BASE_CLASSES = [
    ("goodmem._generated.api.embedders_base", "EmbeddersAPIBase",
     ["create", "get", "list", "update", "delete"]),
    ("goodmem._generated.api.rerankers_base", "RerankersAPIBase",
     ["create", "get", "list", "update", "delete"]),
    ("goodmem._generated.api.llms_base", "LlmsAPIBase",
     ["create", "get", "list", "update", "delete"]),
    ("goodmem._generated.api.spaces_base", "SpacesAPIBase",
     ["create", "get", "list", "update", "delete"]),
    ("goodmem._generated.api.apikeys_base", "ApikeysAPIBase",
     ["create", "list", "update", "delete"]),
    ("goodmem._generated.api.memories_base", "MemoriesAPIBase",
     ["create", "get", "list", "delete", "content", "batch_create",
      "batch_delete", "batch_get", "retrieve"]),
    ("goodmem._generated.api.users_base", "UsersAPIBase",
     ["get", "me"]),
    ("goodmem._generated.api.system_base", "SystemAPIBase",
     ["info", "init"]),
    ("goodmem._generated.api.ocr_base", "OcrAPIBase",
     ["document"]),
    ("goodmem._generated.api.admin_base", "AdminAPIBase",
     ["drain", "background_jobs.purge", "license.reload"]),
]


class TestRestMetadataCompleteness:
    """Every method on every base class must have a _rest_metadata entry."""

    @pytest.mark.parametrize(
        "module_path,class_name,expected_keys",
        _ALL_BASE_CLASSES,
        ids=[c[1] for c in _ALL_BASE_CLASSES],
    )
    def test_has_all_method_keys(self, module_path, class_name, expected_keys):
        import importlib
        mod = importlib.import_module(module_path)
        cls = getattr(mod, class_name)
        meta = cls._rest_metadata
        for key in expected_keys:
            assert key in meta, f"{class_name}._rest_metadata missing key {key!r}"

    @pytest.mark.parametrize(
        "module_path,class_name,expected_keys",
        _ALL_BASE_CLASSES,
        ids=[c[1] for c in _ALL_BASE_CLASSES],
    )
    def test_entries_have_http_method_and_path(self, module_path, class_name, expected_keys):
        import importlib
        mod = importlib.import_module(module_path)
        cls = getattr(mod, class_name)
        meta = cls._rest_metadata
        for key in expected_keys:
            entry = meta[key]
            assert "http_method" in entry, f"{class_name}.{key} missing http_method"
            assert "path" in entry, f"{class_name}.{key} missing path"


class TestRestMetadataCorrectness:
    """Spot-check specific fields for important methods."""

    def test_retrieve_has_accept_from(self):
        from goodmem._generated.api.memories_base import MemoriesAPIBase
        assert MemoriesAPIBase._rest_metadata["retrieve"]["accept_from"] == "format"

    def test_content_has_bytes_response(self):
        from goodmem._generated.api.memories_base import MemoriesAPIBase
        assert MemoriesAPIBase._rest_metadata["content"]["response"] == "bytes"

    def test_admin_sub_namespace_keys_are_dotted(self):
        from goodmem._generated.api.admin_base import AdminAPIBase
        meta = AdminAPIBase._rest_metadata
        assert "background_jobs.purge" in meta
        assert "license.reload" in meta

    def test_batch_create_path(self):
        from goodmem._generated.api.memories_base import MemoriesAPIBase
        assert MemoriesAPIBase._rest_metadata["batch_create"]["path"] == "/v1/memories:batchCreate"

    def test_system_info_is_get(self):
        from goodmem._generated.api.system_base import SystemAPIBase
        assert SystemAPIBase._rest_metadata["info"]["http_method"] == "GET"

    def test_users_me_path(self):
        from goodmem._generated.api.users_base import UsersAPIBase
        assert UsersAPIBase._rest_metadata["me"]["path"] == "/v1/users/me"


# ---------------------------------------------------------------------------
# Keyword-only guard — every named param (except self) must be KEYWORD_ONLY
# ---------------------------------------------------------------------------

class TestAllParamsKeywordOnly:
    """Regression guard: all generated Tier-2 method params must be keyword-only.

    **kwargs is naturally keyword-only and is excluded from this check.
    Methods with no named params (only self, or self + **kwargs) are skipped.
    """

    @pytest.mark.parametrize(
        "module_path,class_name,expected_keys",
        _ALL_BASE_CLASSES,
        ids=[c[1] for c in _ALL_BASE_CLASSES],
    )
    def test_named_params_are_keyword_only(self, module_path, class_name, expected_keys):
        import importlib
        import inspect

        mod = importlib.import_module(module_path)
        cls = getattr(mod, class_name)

        for method_key in expected_keys:
            # Resolve dotted keys (e.g. "background_jobs.purge") to the actual method
            parts = method_key.split(".")
            obj = cls
            for part in parts:
                obj = getattr(obj, part, None)
                if obj is None:
                    break
            if obj is None:
                continue

            fn = obj if callable(obj) else None
            if fn is None:
                continue

            sig = inspect.signature(fn)
            for pname, param in sig.parameters.items():
                if pname == "self":
                    continue
                if param.kind == inspect.Parameter.VAR_KEYWORD:
                    continue  # **kwargs is fine
                assert param.kind == inspect.Parameter.KEYWORD_ONLY, (
                    f"{class_name}.{method_key}() param {pname!r} is "
                    f"{param.kind.name}, expected KEYWORD_ONLY"
                )
