"""High-value tests for security/module_whitelist.py - Module Import Security."""

from unittest.mock import patch, MagicMock

import pytest

from local_deep_research.security.module_whitelist import (
    validate_module_import,
    get_safe_module_class,
    ALLOWED_MODULE_PATHS,
    ALLOWED_CLASS_NAMES,
    ALLOWED_MODULES,
    SecurityError,
    ModuleNotAllowedError,
)


# ---------------------------------------------------------------------------
# ALLOWED_MODULE_PATHS constraints
# ---------------------------------------------------------------------------


class TestAllowedModulePaths:
    """Whitelist structure validation."""

    def test_is_frozenset(self):
        assert isinstance(ALLOWED_MODULE_PATHS, frozenset)

    def test_all_start_with_dot(self):
        for path in ALLOWED_MODULE_PATHS:
            assert path.startswith("."), (
                f"Module path {path!r} must start with '.'"
            )

    def test_legacy_alias_same_object(self):
        assert ALLOWED_MODULES is ALLOWED_MODULE_PATHS

    def test_known_engine_paths_present(self):
        expected = [
            ".engines.search_engine_brave",
            ".engines.search_engine_ddg",
            ".engines.search_engine_arxiv",
            ".engines.search_engine_wikipedia",
            ".engines.search_engine_tavily",
        ]
        for path in expected:
            assert path in ALLOWED_MODULE_PATHS

    def test_not_empty(self):
        assert len(ALLOWED_MODULE_PATHS) > 0


# ---------------------------------------------------------------------------
# ALLOWED_CLASS_NAMES constraints
# ---------------------------------------------------------------------------


class TestAllowedClassNames:
    """Class name whitelist validation."""

    def test_is_frozenset(self):
        assert isinstance(ALLOWED_CLASS_NAMES, frozenset)

    def test_known_classes_present(self):
        expected = [
            "BraveSearchEngine",
            "DuckDuckGoSearchEngine",
            "ArXivSearchEngine",
            "WikipediaSearchEngine",
            "TavilySearchEngine",
            "BaseSearchEngine",
        ]
        for cls_name in expected:
            assert cls_name in ALLOWED_CLASS_NAMES

    def test_not_empty(self):
        assert len(ALLOWED_CLASS_NAMES) > 0


# ---------------------------------------------------------------------------
# SecurityError / ModuleNotAllowedError
# ---------------------------------------------------------------------------


class TestSecurityError:
    def test_is_exception(self):
        assert issubclass(SecurityError, Exception)

    def test_alias_is_same_class(self):
        assert ModuleNotAllowedError is SecurityError

    def test_can_raise_and_catch(self):
        with pytest.raises(SecurityError):
            raise SecurityError("blocked")


# ---------------------------------------------------------------------------
# validate_module_import()
# ---------------------------------------------------------------------------


class TestValidateModuleImport:
    """Module + class pair validation."""

    def test_valid_pair_accepted(self):
        assert (
            validate_module_import(
                ".engines.search_engine_brave", "BraveSearchEngine"
            )
            is True
        )

    def test_non_relative_path_rejected(self):
        assert validate_module_import("os", "system") is False

    def test_absolute_dotted_path_rejected(self):
        assert (
            validate_module_import(
                "local_deep_research.web_search_engines.engines.search_engine_brave",
                "BraveSearchEngine",
            )
            is False
        )

    def test_empty_module_rejected(self):
        assert validate_module_import("", "BraveSearchEngine") is False

    def test_empty_class_rejected(self):
        assert (
            validate_module_import(".engines.search_engine_brave", "") is False
        )

    def test_both_empty_rejected(self):
        assert validate_module_import("", "") is False

    def test_unlisted_module_rejected(self):
        assert (
            validate_module_import(
                ".engines.search_engine_evil", "BraveSearchEngine"
            )
            is False
        )

    def test_unlisted_class_rejected(self):
        assert (
            validate_module_import(".engines.search_engine_brave", "EvilEngine")
            is False
        )

    def test_both_unlisted_rejected(self):
        assert validate_module_import(".evil_module", "EvilClass") is False


# ---------------------------------------------------------------------------
# get_safe_module_class()
# ---------------------------------------------------------------------------


class TestGetSafeModuleClass:
    """Safe dynamic import."""

    def test_invalid_module_raises_security_error(self):
        with pytest.raises(
            SecurityError, match="not in the security whitelist"
        ):
            get_safe_module_class("os", "system")

    def test_invalid_class_raises_security_error(self):
        with pytest.raises(
            SecurityError, match="not in the security whitelist"
        ):
            get_safe_module_class(".engines.search_engine_brave", "EvilClass")

    def test_whitelisted_class_non_whitelisted_module_rejected(self):
        """Whitelisted class name with non-whitelisted module still raises."""
        with pytest.raises(
            SecurityError, match="not in the security whitelist"
        ):
            get_safe_module_class(
                ".engines.search_engine_evil", "BraveSearchEngine"
            )

    @patch(
        "local_deep_research.security.module_whitelist.importlib.import_module"
    )
    def test_valid_pair_imports_and_returns_class(self, mock_import):
        mock_module = MagicMock()
        mock_module.BraveSearchEngine = type("BraveSearchEngine", (), {})
        mock_import.return_value = mock_module

        cls = get_safe_module_class(
            ".engines.search_engine_brave", "BraveSearchEngine"
        )
        assert cls is mock_module.BraveSearchEngine
        mock_import.assert_called_once_with(
            ".engines.search_engine_brave",
            package="local_deep_research.web_search_engines",
        )

    @patch(
        "local_deep_research.security.module_whitelist.importlib.import_module"
    )
    def test_custom_package_used(self, mock_import):
        mock_module = MagicMock()
        mock_module.BraveSearchEngine = type("BraveSearchEngine", (), {})
        mock_import.return_value = mock_module

        get_safe_module_class(
            ".engines.search_engine_brave",
            "BraveSearchEngine",
            package="custom.package",
        )
        mock_import.assert_called_once_with(
            ".engines.search_engine_brave", package="custom.package"
        )

    @patch(
        "local_deep_research.security.module_whitelist.importlib.import_module"
    )
    def test_module_not_found_propagates(self, mock_import):
        mock_import.side_effect = ModuleNotFoundError("No module")
        with pytest.raises(ModuleNotFoundError):
            get_safe_module_class(
                ".engines.search_engine_brave", "BraveSearchEngine"
            )

    @patch(
        "local_deep_research.security.module_whitelist.importlib.import_module"
    )
    def test_attribute_error_propagates(self, mock_import):
        mock_module = MagicMock(spec=[])  # no attributes
        mock_import.return_value = mock_module
        with pytest.raises(AttributeError):
            get_safe_module_class(
                ".engines.search_engine_brave", "BraveSearchEngine"
            )
