"""Validation tests for the convenience registry."""

from __future__ import annotations

import pytest

from goodmem._registries import PROVIDER_REGISTRY
from goodmem._transforms import (
    CONVENIENCE_REGISTRY,
    apply_convenience_transforms,
)


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------

def _valid_slug_method_pairs() -> set[tuple[str, str]]:
    """Return all valid (slug, method) pairs from ROUTE_BINDINGS."""
    from _doc_gen.generate_ref import PUBLIC_METHODS
    pairs: set[tuple[str, str]] = set()
    for slug, methods in PUBLIC_METHODS.items():
        for m in methods:
            pairs.add((slug, m))
    return pairs


# ---------------------------------------------------------------------------
# Test 1: Registry keys match valid (slug, method) combinations
# ---------------------------------------------------------------------------

class TestRegistryKeysValid:
    def test_all_keys_are_valid_slug_method_pairs(self):
        valid = _valid_slug_method_pairs()
        for key in CONVENIENCE_REGISTRY:
            assert key in valid, f"Registry key {key} is not a valid (slug, method) pair"


# ---------------------------------------------------------------------------
# Test 2: Every param has valid type and description
# ---------------------------------------------------------------------------

class TestParamStructure:
    @pytest.fixture
    def all_params(self):
        """Yield (key, pname, pspec) for every registered param."""
        result = []
        for key, entry in CONVENIENCE_REGISTRY.items():
            for pname, pspec in entry.get("params", {}).items():
                result.append((key, pname, pspec))
        return result

    def test_param_type_is_valid_when_present(self, all_params):
        for key, pname, pspec in all_params:
            if "type" in pspec:
                assert isinstance(pspec["type"], str), f"{key}.{pname} type must be str"
                assert pspec["type"], f"{key}.{pname} has empty type"

    def test_every_param_has_description_or_inherits(self, all_params):
        from _doc_gen.griffe_loader import _load_tier2_docs_from_ir
        tier2 = _load_tier2_docs_from_ir()
        for key, pname, pspec in all_params:
            if "description" in pspec:
                assert isinstance(pspec["description"], str), f"{key}.{pname} description must be str"
                assert pspec["description"], f"{key}.{pname} has empty description"
            else:
                # Must inherit from a same-name Tier-2 param
                t2_params = {p["name"] for p in tier2.get(key, {}).get("params", [])}
                assert pname in t2_params, (
                    f"{key}.{pname} has no description and no same-name Tier-2 param to inherit from"
                )

    def test_no_type_on_model_field_params(self, all_params):
        """Params that exist on the pydantic request model must NOT declare 'type'
        unless the declared type intentionally differs from the model's type
        (e.g. Tier-3 uses a convenience subclass).

        Types for model-field params are derived automatically from the model.
        Declaring them in convenience.py would be redundant and could silently
        drift out of sync with the actual model definition.
        """
        from _doc_gen.generate_ref import _resolve_request_model
        from _client_gen.param_merge import _model_field_type_map

        for key, pname, pspec in all_params:
            if "type" not in pspec:
                continue
            slug, method_name = key
            model_cls = _resolve_request_model(slug, method_name)
            if model_cls is None or not hasattr(model_cls, "model_fields"):
                continue
            if pname not in model_cls.model_fields:
                continue
            # Allow explicit type when it intentionally differs from the model
            model_types = _model_field_type_map(model_cls)
            model_type = model_types.get(pname, "")
            if pspec["type"] != model_type:
                continue  # intentional override (e.g. convenience subclass)
            assert False, (
                f"{key}.{pname} declares 'type' = {pspec['type']!r} but it "
                f"matches the model field on {model_cls.__name__} — remove "
                f"'type' from convenience.py (it is derived automatically)"
            )

    def test_no_legacy_suppresses(self, all_params):
        """Legacy 'suppresses' has been replaced by 'maps_to'/'replaces'."""
        for key, entry in CONVENIENCE_REGISTRY.items():
            assert "suppresses" not in entry, (
                f"{key} uses legacy 'suppresses' — use 'maps_to' or 'replaces' instead"
            )

    def test_maps_to_and_replaces_are_mutually_exclusive(self, all_params):
        """A param should use maps_to (parallel) or replaces (full alias), not both."""
        for key, entry in CONVENIENCE_REGISTRY.items():
            for pname, pspec in entry.get("params", {}).items():
                assert not (pspec.get("maps_to") and pspec.get("replaces")), (
                    f"{key}.{pname} has both 'maps_to' and 'replaces' — use one or the other"
                )


# ---------------------------------------------------------------------------
# Test 3: Descriptions are non-empty strings
# ---------------------------------------------------------------------------

class TestDescriptions:
    def test_entry_descriptions_are_strings_or_absent(self):
        for key, entry in CONVENIENCE_REGISTRY.items():
            desc = entry.get("description")
            if desc is not None:
                assert isinstance(desc, str), f"{key} description must be str"
                assert desc.strip(), f"{key} has empty description"


# ---------------------------------------------------------------------------
# Test 4: maps_to paths are valid
# ---------------------------------------------------------------------------

class TestMapsToValid:
    def test_maps_to_root_is_tier2_param(self):
        """The root segment of maps_to must be a Tier-2 param name."""
        from _doc_gen.griffe_loader import _load_tier2_docs_from_ir

        tier2 = _load_tier2_docs_from_ir()

        for key, entry in CONVENIENCE_REGISTRY.items():
            t2_params = {p["name"] for p in tier2.get(key, {}).get("params", [])}
            for pname, pspec in entry.get("params", {}).items():
                maps_to = pspec.get("maps_to")
                if not maps_to:
                    continue
                root = maps_to.split(".")[0]
                assert root in t2_params, (
                    f"{key}.{pname} maps_to root '{root}' is not a Tier-2 "
                    f"param for {key}. Known: {sorted(t2_params)}"
                )

    def test_maps_to_is_string_when_present(self):
        for key, entry in CONVENIENCE_REGISTRY.items():
            for pname, pspec in entry.get("params", {}).items():
                maps_to = pspec.get("maps_to")
                if maps_to is not None:
                    assert isinstance(maps_to, str), (
                        f"{key}.{pname} maps_to must be str"
                    )
                    assert maps_to, f"{key}.{pname} has empty maps_to"

    def test_maps_to_required_inference(self):
        """Verify required is correctly derived from the model path."""
        from _doc_gen.generate_ref import _resolve_request_model
        from _client_gen.param_merge import _is_path_all_required

        # space_ids maps to space_keys (required on RetrieveMemoryRequest)
        model = _resolve_request_model("memories", "retrieve")
        assert model is not None
        assert _is_path_all_required(model, "space_keys") is True

        # llm_id maps through post_processor (optional) → should be False
        assert _is_path_all_required(model, "post_processor.config.llm_id") is False


# ---------------------------------------------------------------------------
# Test 5: Entry transforms are callables or absent
# ---------------------------------------------------------------------------

class TestTransformStructure:
    def test_entry_transforms_are_lists(self):
        for key, entry in CONVENIENCE_REGISTRY.items():
            transforms = entry.get("transforms")
            if transforms is not None:
                assert isinstance(transforms, list), f"{key} transforms must be a list"
                for t in transforms:
                    assert "kind" in t, f"{key} transform missing 'kind': {t}"

    def test_entries_with_transforms(self):
        """Verify all entries that should have transforms do have them."""
        expected_transforms = {
            ("memories", "retrieve"),
            ("embedders", "create"),
            ("llms", "create"),
            ("rerankers", "create"),
            ("spaces", "create"),
        }
        actual = {k for k, v in CONVENIENCE_REGISTRY.items() if v.get("transforms")}
        assert actual == expected_transforms


# ---------------------------------------------------------------------------
# Test 6: Runtime driver smoke tests
# ---------------------------------------------------------------------------

class TestApplyConvenienceTransforms:
    def test_space_ids_transform(self):
        kwargs = {"space_ids": ["abc", "def"]}
        apply_convenience_transforms(("memories", "retrieve"), kwargs)
        assert "space_ids" not in kwargs
        assert kwargs["space_keys"] == [{"space_id": "abc"}, {"space_id": "def"}]

    def test_space_ids_single_element_list(self):
        kwargs = {"space_ids": ["abc"]}
        apply_convenience_transforms(("memories", "retrieve"), kwargs)
        assert kwargs["space_keys"] == [{"space_id": "abc"}]

    def test_post_processor_flattening(self):
        kwargs = {"llm_id": "llm-1", "reranker_id": "rr-1", "llm_temp": 0.5}
        apply_convenience_transforms(("memories", "retrieve"), kwargs)
        assert "llm_id" not in kwargs
        assert "reranker_id" not in kwargs
        pp = kwargs["post_processor"]
        assert pp["config"]["llm_id"] == "llm-1"
        assert pp["config"]["reranker_id"] == "rr-1"
        assert pp["config"]["llm_temp"] == 0.5

    def test_explicit_post_processor_wins_over_flat_keys(self):
        """Explicit post_processor takes priority — flat keys are silently dropped."""
        kwargs = {"llm_id": "llm-1", "post_processor": {"name": "custom"}}
        apply_convenience_transforms(("memories", "retrieve"), kwargs)
        assert kwargs["post_processor"] == {"name": "custom"}
        assert "llm_id" not in kwargs

    def test_no_op_for_unknown_key(self):
        kwargs = {"foo": "bar"}
        apply_convenience_transforms(("unknown", "method"), kwargs)
        assert kwargs == {"foo": "bar"}

    def test_no_op_when_no_transform_params_present(self):
        kwargs = {"message": "hello"}
        apply_convenience_transforms(("memories", "retrieve"), kwargs)
        assert kwargs == {"message": "hello"}

    def test_embedder_create_transform(self):
        kwargs = {"model_identifier": "text-embedding-3-small", "api_key": "sk-test"}
        apply_convenience_transforms(("embedders", "create"), kwargs)
        assert kwargs["provider_type"] == "OPENAI"
        assert kwargs["distribution_type"] == "DENSE"
        assert "api_key" not in kwargs
        assert "credentials" in kwargs

    def test_space_create_transform(self):
        kwargs = {"name": "Test"}
        apply_convenience_transforms(("spaces", "create"), kwargs)
        assert "default_chunking_config" in kwargs

    # -- Type validation (workaround for #870) --

    def test_type_validation_rejects_wrong_scalar(self):
        kwargs = {"llm_temp": "not-a-float"}
        with pytest.raises(TypeError, match="'llm_temp' expected float, got str"):
            apply_convenience_transforms(("memories", "retrieve"), kwargs)

    def test_type_validation_accepts_int_for_float(self):
        kwargs = {"llm_temp": 1}
        apply_convenience_transforms(("memories", "retrieve"), kwargs)
        # Should pass — int is accepted for float params.

    def test_type_validation_rejects_bool_for_int(self):
        kwargs = {"gen_token_budget": True}
        with pytest.raises(TypeError, match="'gen_token_budget' expected int, got bool"):
            apply_convenience_transforms(("memories", "retrieve"), kwargs)

    def test_type_validation_rejects_wrong_list_type(self):
        kwargs = {"space_ids": 123}
        with pytest.raises(TypeError, match=r"'space_ids' expected list\[str\], got int"):
            apply_convenience_transforms(("memories", "retrieve"), kwargs)

    def test_type_validation_rejects_wrong_list_element(self):
        kwargs = {"space_ids": ["ok", 42]}
        with pytest.raises(TypeError, match=r"'space_ids\[1\]' expected str, got int"):
            apply_convenience_transforms(("memories", "retrieve"), kwargs)

    def test_type_validation_skips_params_without_type(self):
        # model_identifier has no "type" in the registry — no validation.
        kwargs = {"model_identifier": 12345, "api_key": "sk-test"}
        apply_convenience_transforms(("embedders", "create"), kwargs)
        # Should not raise TypeError for model_identifier.


# ---------------------------------------------------------------------------
# Test 7: Mutual exclusivity — api_key + credentials across namespaces
# ---------------------------------------------------------------------------

class TestApiKeyCredentialsMutualExclusivity:
    """api_key + credentials must raise ValueError for all 3 resource types."""

    def test_embedder_api_key_and_credentials_raises(self):
        kwargs = {"api_key": "sk-test", "credentials": {"kind": "CUSTOM"}}
        with pytest.raises(ValueError, match="api_key.*credentials"):
            apply_convenience_transforms(("embedders", "create"), kwargs)

    def test_llm_api_key_and_credentials_raises(self):
        kwargs = {"api_key": "sk-test", "credentials": {"kind": "CUSTOM"}}
        with pytest.raises(ValueError, match="api_key.*credentials"):
            apply_convenience_transforms(("llms", "create"), kwargs)

    def test_reranker_api_key_and_credentials_raises(self):
        kwargs = {"api_key": "sk-test", "credentials": {"kind": "CUSTOM"}}
        with pytest.raises(ValueError, match="api_key.*credentials"):
            apply_convenience_transforms(("rerankers", "create"), kwargs)

    def test_embedder_api_key_alone_ok(self):
        kwargs = {"api_key": "sk-test", "model_identifier": "text-embedding-3-large"}
        apply_convenience_transforms(("embedders", "create"), kwargs)
        assert "credentials" in kwargs
        assert "api_key" not in kwargs

    def test_llm_api_key_alone_ok(self):
        kwargs = {"api_key": "sk-test", "model_identifier": "gpt-4o-mini"}
        apply_convenience_transforms(("llms", "create"), kwargs)
        assert "credentials" in kwargs
        assert "api_key" not in kwargs

    def test_reranker_api_key_alone_ok(self):
        kwargs = {"api_key": "sk-test", "model_identifier": "rerank-2"}
        apply_convenience_transforms(("rerankers", "create"), kwargs)
        assert "credentials" in kwargs
        assert "api_key" not in kwargs


# ---------------------------------------------------------------------------
# Test 8: Model registry — auto-fill, explicit overrides, unknown models
# ---------------------------------------------------------------------------

class TestModelRegistryBehavior:
    """Verify _apply_model_registry via apply_convenience_transforms."""

    def test_known_embedder_auto_fills(self):
        """Known model auto-fills provider_type, dimensionality, etc."""
        kwargs = {"model_identifier": "text-embedding-3-large", "api_key": "sk-test"}
        apply_convenience_transforms(("embedders", "create"), kwargs)
        assert kwargs["provider_type"] == "OPENAI"
        assert "endpoint_url" in kwargs
        assert "openai" in kwargs["endpoint_url"].lower()
        assert kwargs["dimensionality"] == 1536
        assert kwargs["distribution_type"] == "DENSE"

    def test_known_embedder_explicit_overrides_prevail(self):
        """User-provided values must not be overwritten by registry defaults."""
        kwargs = {
            "model_identifier": "text-embedding-3-large",
            "api_key": "sk-test",
            "dimensionality": 256,
            "provider_type": "CUSTOM_PROVIDER",
            "endpoint_url": "http://custom-endpoint/v1",
        }
        apply_convenience_transforms(("embedders", "create"), kwargs)
        assert kwargs["dimensionality"] == 256
        assert kwargs["provider_type"] == "CUSTOM_PROVIDER"
        assert kwargs["endpoint_url"] == "http://custom-endpoint/v1"

    def test_known_llm_auto_fills(self):
        """Known LLM model auto-fills provider_type, endpoint_url, etc."""
        kwargs = {"model_identifier": "gpt-4o-mini", "api_key": "sk-test"}
        apply_convenience_transforms(("llms", "create"), kwargs)
        assert kwargs["provider_type"] == "OPENAI"
        assert "endpoint_url" in kwargs

    def test_known_llm_explicit_overrides_prevail(self):
        kwargs = {
            "model_identifier": "gpt-4o-mini",
            "api_key": "sk-test",
            "provider_type": "VLLM",
            "endpoint_url": "http://my-server/v1",
        }
        apply_convenience_transforms(("llms", "create"), kwargs)
        assert kwargs["provider_type"] == "VLLM"
        assert kwargs["endpoint_url"] == "http://my-server/v1"

    def test_known_reranker_explicit_overrides_prevail(self):
        kwargs = {
            "model_identifier": "rerank-2",
            "api_key": "sk-test",
            "provider_type": "CUSTOM",
            "endpoint_url": "http://custom-rerank/v1",
        }
        apply_convenience_transforms(("rerankers", "create"), kwargs)
        assert kwargs["provider_type"] == "CUSTOM"
        assert kwargs["endpoint_url"] == "http://custom-rerank/v1"

    def test_unknown_embedder_no_autofill(self):
        """Unknown model_identifier should not auto-fill any fields."""
        kwargs = {"model_identifier": "my-custom-model-xyz", "api_key": "sk-test"}
        apply_convenience_transforms(("embedders", "create"), kwargs)
        # Only distribution_type gets a default (from _transform_embedder_create, not registry)
        assert kwargs.get("distribution_type") == "DENSE"
        assert "provider_type" not in kwargs
        assert "dimensionality" not in kwargs
        assert "endpoint_url" not in kwargs

    def test_unknown_llm_no_autofill(self):
        kwargs = {"model_identifier": "my-custom-llm-xyz", "api_key": "sk-test"}
        apply_convenience_transforms(("llms", "create"), kwargs)
        assert "provider_type" not in kwargs
        assert "endpoint_url" not in kwargs

    def test_unknown_reranker_no_autofill(self):
        kwargs = {"model_identifier": "my-custom-reranker-xyz", "api_key": "sk-test"}
        apply_convenience_transforms(("rerankers", "create"), kwargs)
        assert "provider_type" not in kwargs
        assert "endpoint_url" not in kwargs


# ---------------------------------------------------------------------------
# Test 8b: Unknown model + missing required fields → ValidationError on create
# ---------------------------------------------------------------------------

class TestUnknownModelCreateFails:
    """Creating a resource with an unknown model and missing required fields
    must raise pydantic ValidationError *before* any HTTP call is made."""

    @pytest.fixture
    def client(self):
        from goodmem import Goodmem
        c = Goodmem(base_url="http://localhost:1", api_key="fake")
        yield c
        c.close()

    def test_unknown_embedder_missing_required_fields(self, client):
        """Unknown embedder model → provider_type, endpoint_url, dimensionality not inferred."""
        from pydantic import ValidationError
        with pytest.raises(ValidationError) as exc_info:
            client.embedders.create(
                display_name="Test Embedder",
                model_identifier="my-custom-model-xyz",
                api_key="sk-test",
            )
        errors = {e["loc"][0] for e in exc_info.value.errors()}
        assert "provider_type" in errors or "providerType" in errors
        assert "endpoint_url" in errors or "endpointUrl" in errors
        assert "dimensionality" in errors

    def test_unknown_llm_missing_required_fields(self, client):
        """Unknown LLM model → provider_type, endpoint_url not inferred."""
        from pydantic import ValidationError
        with pytest.raises(ValidationError) as exc_info:
            client.llms.create(
                display_name="Test LLM",
                model_identifier="my-custom-llm-xyz",
                api_key="sk-test",
            )
        errors = {e["loc"][0] for e in exc_info.value.errors()}
        assert "provider_type" in errors or "providerType" in errors
        assert "endpoint_url" in errors or "endpointUrl" in errors

    def test_unknown_reranker_missing_required_fields(self, client):
        """Unknown reranker model → provider_type, endpoint_url not inferred."""
        from pydantic import ValidationError
        with pytest.raises(ValidationError) as exc_info:
            client.rerankers.create(
                display_name="Test Reranker",
                model_identifier="my-custom-reranker-xyz",
                api_key="sk-test",
            )
        errors = {e["loc"][0] for e in exc_info.value.errors()}
        assert "provider_type" in errors or "providerType" in errors
        assert "endpoint_url" in errors or "endpointUrl" in errors

    def test_unknown_embedder_with_all_required_fields_passes_validation(self):
        """Unknown model with all required fields explicitly provided should pass
        pydantic validation (i.e. _prepare_create_payload succeeds)."""
        from goodmem._base import _prepare_create_payload
        from goodmem.models.embedder_creation_request import EmbedderCreationRequest
        kwargs = {
            "display_name": "Test Embedder",
            "model_identifier": "my-custom-model-xyz",
            "provider_type": "OPENAI",
            "endpoint_url": "http://my-custom-embed/v1",
            "dimensionality": 768,
            "distribution_type": "DENSE",
            "api_key": "sk-test",
        }
        apply_convenience_transforms(("embedders", "create"), kwargs)
        # Should not raise — all required fields are present
        json_payload, _, _ = _prepare_create_payload(
            create_model=EmbedderCreationRequest,
            op={},
            kwargs=kwargs,
        )
        assert json_payload is not None
        assert json_payload["modelIdentifier"] == "my-custom-model-xyz"


# ---------------------------------------------------------------------------
# Test 9: memories.create — three-way content source mutual exclusivity
# ---------------------------------------------------------------------------

class TestMemoryCreateContentSources:
    """file_path, original_content, and original_content_b64 are mutually exclusive."""

    @pytest.fixture
    def client(self):
        """Goodmem client with fake URL — ValueError fires before any network call."""
        from goodmem import Goodmem
        c = Goodmem(base_url="http://localhost:1", api_key="fake")
        yield c
        c.close()

    def test_file_path_and_original_content_raises(self, client):
        with pytest.raises(ValueError, match="exactly one"):
            client.memories.create(
                space_id="fake",
                file_path="/tmp/fake.pdf",
                original_content="hello",
            )

    def test_file_path_and_original_content_b64_raises(self, client):
        with pytest.raises(ValueError, match="exactly one"):
            client.memories.create(
                space_id="fake",
                file_path="/tmp/fake.pdf",
                original_content_b64="aGVsbG8=",
            )

    def test_original_content_and_original_content_b64_raises(self, client):
        with pytest.raises(ValueError, match="exactly one"):
            client.memories.create(
                space_id="fake",
                original_content="hello",
                original_content_b64="aGVsbG8=",
            )

    def test_all_three_raises(self, client):
        with pytest.raises(ValueError, match="exactly one"):
            client.memories.create(
                space_id="fake",
                file_path="/tmp/fake.pdf",
                original_content="hello",
                original_content_b64="aGVsbG8=",
            )


# ---------------------------------------------------------------------------
# Test 10: PROVIDER_REGISTRY — structure and content validation
# ---------------------------------------------------------------------------

class TestProviderRegistry:
    def test_provider_registry_loads(self):
        assert isinstance(PROVIDER_REGISTRY, dict)
        assert len(PROVIDER_REGISTRY) > 0

    def test_known_providers_present(self):
        for provider in ("OPENAI", "COHERE", "VOYAGE", "JINA"):
            assert provider in PROVIDER_REGISTRY, f"{provider} missing from PROVIDER_REGISTRY"

    def test_all_providers_have_default_base_url(self):
        for provider, meta in PROVIDER_REGISTRY.items():
            assert "default_base_url" in meta, f"{provider} missing default_base_url"
            assert isinstance(meta["default_base_url"], str), f"{provider} default_base_url must be str"
            assert meta["default_base_url"].startswith("https://"), f"{provider} default_base_url must be https"

    def test_all_providers_have_base_urls_requiring_api_key(self):
        for provider, meta in PROVIDER_REGISTRY.items():
            assert "base_urls_requiring_api_key" in meta, f"{provider} missing base_urls_requiring_api_key"
            assert isinstance(meta["base_urls_requiring_api_key"], list), (
                f"{provider} base_urls_requiring_api_key must be a list"
            )
            assert len(meta["base_urls_requiring_api_key"]) > 0, (
                f"{provider} base_urls_requiring_api_key must not be empty"
            )

    def test_openai_includes_compatible_saas_hosts(self):
        openai_urls = PROVIDER_REGISTRY["OPENAI"]["base_urls_requiring_api_key"]
        assert "https://api.openai.com/v1" in openai_urls
        assert "https://api.anthropic.com/v1" in openai_urls
        assert "https://generativelanguage.googleapis.com/v1beta/openai" in openai_urls
        assert "https://api.mistral.ai/v1" in openai_urls

    def test_cohere_default_url(self):
        assert PROVIDER_REGISTRY["COHERE"]["default_base_url"] == "https://api.cohere.com"

    def test_jina_default_url(self):
        assert PROVIDER_REGISTRY["JINA"]["default_base_url"] == "https://api.jina.ai"

    def test_voyage_default_url(self):
        assert PROVIDER_REGISTRY["VOYAGE"]["default_base_url"] == "https://api.voyageai.com/v1"
