"""Tests for the hardcoded engine registry.

Validates that registry entries are consistent with the security whitelist
and with the default settings JSON files.
"""

import json
from pathlib import Path

import pytest

from local_deep_research.security.module_whitelist import (
    ALLOWED_CLASS_NAMES,
    ALLOWED_MODULE_PATHS,
)
from local_deep_research.web_search_engines.engine_registry import (
    ENGINE_REGISTRY,
    EngineEntry,
    get_engine_entry,
)


class TestRegistryIntegrity:
    """All registry entries must be consistent with the security whitelist."""

    def test_all_module_paths_are_relative(self):
        """Every module_path must start with '.' (relative import)."""
        for name, entry in ENGINE_REGISTRY.items():
            assert entry.module_path.startswith("."), (
                f"Engine '{name}' has non-relative module_path: {entry.module_path}"
            )

    def test_all_module_paths_in_whitelist(self):
        """Every module_path must be in ALLOWED_MODULE_PATHS."""
        for name, entry in ENGINE_REGISTRY.items():
            assert entry.module_path in ALLOWED_MODULE_PATHS, (
                f"Engine '{name}' module_path {entry.module_path!r} "
                f"not in ALLOWED_MODULE_PATHS"
            )

    def test_all_class_names_in_whitelist(self):
        """Every class_name must be in ALLOWED_CLASS_NAMES."""
        for name, entry in ENGINE_REGISTRY.items():
            assert entry.class_name in ALLOWED_CLASS_NAMES, (
                f"Engine '{name}' class_name {entry.class_name!r} "
                f"not in ALLOWED_CLASS_NAMES"
            )

    def test_full_search_fields_consistent(self):
        """full_search_module and full_search_class must both be set or both be None."""
        for name, entry in ENGINE_REGISTRY.items():
            has_module = entry.full_search_module is not None
            has_class = entry.full_search_class is not None
            assert has_module == has_class, (
                f"Engine '{name}' has inconsistent full_search fields: "
                f"module={entry.full_search_module}, class={entry.full_search_class}"
            )

    def test_full_search_module_paths_in_whitelist(self):
        """full_search_module values must be in ALLOWED_MODULE_PATHS."""
        for name, entry in ENGINE_REGISTRY.items():
            if entry.full_search_module:
                assert entry.full_search_module in ALLOWED_MODULE_PATHS, (
                    f"Engine '{name}' full_search_module "
                    f"{entry.full_search_module!r} not in ALLOWED_MODULE_PATHS"
                )

    def test_full_search_class_names_in_whitelist(self):
        """full_search_class values must be in ALLOWED_CLASS_NAMES."""
        for name, entry in ENGINE_REGISTRY.items():
            if entry.full_search_class:
                assert entry.full_search_class in ALLOWED_CLASS_NAMES, (
                    f"Engine '{name}' full_search_class "
                    f"{entry.full_search_class!r} not in ALLOWED_CLASS_NAMES"
                )


class TestRegistryCoverage:
    """Registry should cover all engines that were previously in settings JSON."""

    @pytest.fixture()
    def default_settings(self):
        """Load the default_settings.json file."""
        path = (
            Path(__file__).resolve().parents[2]
            / "src"
            / "local_deep_research"
            / "defaults"
            / "default_settings.json"
        )
        with open(path) as f:
            return json.load(f)

    def test_covers_all_web_engines_in_defaults(self, default_settings):
        """Registry should have entries for all web engines in default_settings.json."""
        engine_names = set()
        for key in default_settings:
            if key.startswith("search.engine.web."):
                parts = key.split(".")
                if len(parts) >= 4:
                    engine_names.add(parts[3])

        for name in engine_names:
            assert name in ENGINE_REGISTRY, (
                f"Engine '{name}' from default_settings.json missing from registry"
            )

    def test_covers_all_web_engines_in_per_engine_settings(self):
        """Registry should have entries for all engines with per-engine settings files."""
        defaults_dir = (
            Path(__file__).resolve().parents[2]
            / "src"
            / "local_deep_research"
            / "defaults"
        )
        # Runtime-registered engines that are intentionally not in the registry
        runtime_engines = {"library", "collection"}

        engine_names = set()
        for settings_file in sorted(defaults_dir.glob("settings_*.json")):
            with open(settings_file) as f:
                data = json.load(f)
            for key in data:
                if key.startswith("search.engine.web."):
                    parts = key.split(".")
                    if len(parts) >= 4:
                        engine_names.add(parts[3])

        for name in engine_names - runtime_engines:
            assert name in ENGINE_REGISTRY, (
                f"Engine '{name}' from per-engine settings file missing from registry"
            )

    def test_no_settings_files_contain_module_paths(self):
        """Settings files should not contain module_path/class_name (now in registry)."""
        defaults_dir = (
            Path(__file__).resolve().parents[2]
            / "src"
            / "local_deep_research"
            / "defaults"
        )
        for settings_file in sorted(defaults_dir.glob("*.json")):
            with open(settings_file) as f:
                content = f.read()
            for field in ("module_path", "class_name"):
                assert field not in content, (
                    f"{settings_file.name} still contains '{field}' — "
                    f"these should be in engine_registry.py instead"
                )

    def test_removed_meta_engines_not_in_registry(self):
        """The removed meta engines must not reappear in the registry."""
        assert "auto" not in ENGINE_REGISTRY
        assert "meta" not in ENGINE_REGISTRY
        assert "parallel" not in ENGINE_REGISTRY
        assert "parallel_scientific" not in ENGINE_REGISTRY


class TestEngineEntryDataclass:
    """Test the EngineEntry dataclass."""

    def test_frozen(self):
        """EngineEntry should be immutable."""
        entry = EngineEntry(
            module_path=".engines.test",
            class_name="TestEngine",
        )
        with pytest.raises(AttributeError):
            entry.module_path = "changed"

    def test_optional_full_search_defaults_to_none(self):
        """full_search fields default to None."""
        entry = EngineEntry(
            module_path=".engines.test",
            class_name="TestEngine",
        )
        assert entry.full_search_module is None
        assert entry.full_search_class is None

    def test_full_search_fields_set(self):
        """full_search fields can be set."""
        entry = EngineEntry(
            module_path=".engines.test",
            class_name="TestEngine",
            full_search_module=".engines.full_search",
            full_search_class="FullSearchResults",
        )
        assert entry.full_search_module == ".engines.full_search"
        assert entry.full_search_class == "FullSearchResults"


class TestGetEngineEntry:
    """Test the get_engine_entry helper."""

    def test_returns_entry_for_known_engine(self):
        """Should return EngineEntry for known engine names."""
        entry = get_engine_entry("brave")
        assert entry is not None
        assert entry.class_name == "BraveSearchEngine"

    def test_returns_none_for_unknown_engine(self):
        """Should return None for unknown engine names."""
        assert get_engine_entry("nonexistent_engine") is None

    def test_returns_none_for_runtime_engines(self):
        """Runtime-registered engines (library, collection_*) are not in registry."""
        assert get_engine_entry("library") is None
        assert get_engine_entry("collection_1") is None


class TestSearchConfigInjection:
    """Test that search_config() injects registry data."""

    def test_search_config_injects_module_path(self):
        """search_config() should inject module_path from registry."""
        from unittest.mock import patch

        from local_deep_research.web_search_engines.search_engines_config import (
            search_config,
        )

        def mock_get_setting(key, default, **kwargs):
            if key == "search.engine.web":
                return {
                    "brave.requires_api_key": True,
                    "brave.api_key": "test-key",
                }
            return default

        with (
            patch(
                "local_deep_research.web_search_engines.search_engines_config._get_setting",
                side_effect=mock_get_setting,
            ),
            patch(
                "local_deep_research.web_search_engines.retriever_registry.retriever_registry"
            ) as mock_registry,
        ):
            mock_registry.list_registered.return_value = []
            result = search_config()

        assert "brave" in result
        assert result["brave"]["module_path"] == ".engines.search_engine_brave"
        assert result["brave"]["class_name"] == "BraveSearchEngine"
        assert result["brave"]["full_search_module"] == ".engines.full_search"
        assert result["brave"]["full_search_class"] == "FullSearchResults"

    def test_removed_meta_engines_absent_from_search_config(self):
        """search_config() should not contain the removed meta engines."""
        from unittest.mock import patch

        from local_deep_research.web_search_engines.search_engines_config import (
            search_config,
        )

        def mock_get_setting(key, default, **kwargs):
            return default

        with (
            patch(
                "local_deep_research.web_search_engines.search_engines_config._get_setting",
                side_effect=mock_get_setting,
            ),
            patch(
                "local_deep_research.web_search_engines.retriever_registry.retriever_registry"
            ) as mock_registry,
        ):
            mock_registry.list_registered.return_value = []
            result = search_config()

        assert "auto" not in result
        assert "meta" not in result
        assert "parallel" not in result
        assert "parallel_scientific" not in result
