"""
Comprehensive tests for local_deep_research.database.sqlcipher_compat module.

Tests the compatibility layer logic, feature detection, and ImportError
behavior when sqlcipher3 is unavailable.
"""

import importlib
import sys
import types
from unittest import mock

import pytest


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

MODULE_PATH = "local_deep_research.database.sqlcipher_compat"


def _fresh_import():
    """Re-import sqlcipher_compat so module-level code re-executes."""
    mod = importlib.import_module(MODULE_PATH)
    importlib.reload(mod)
    return mod


# ---------------------------------------------------------------------------
# 1. Successful import path – sqlcipher3 is available
# ---------------------------------------------------------------------------


class TestGetSqlcipherModuleSuccess:
    """When sqlcipher3 can be imported, get_sqlcipher_module should return it."""

    def test_returns_sqlcipher3_module(self):
        """get_sqlcipher_module returns the sqlcipher3 module object."""
        fake_sqlcipher3 = types.ModuleType("sqlcipher3")
        fake_sqlcipher3.dbapi2 = object()

        with mock.patch.dict(sys.modules, {"sqlcipher3": fake_sqlcipher3}):
            mod = _fresh_import()
            result = mod.get_sqlcipher_module()
            assert result is fake_sqlcipher3

    def test_returned_module_has_dbapi2(self):
        """The returned module should carry a dbapi2 attribute."""
        fake_sqlcipher3 = types.ModuleType("sqlcipher3")
        sentinel = object()
        fake_sqlcipher3.dbapi2 = sentinel

        with mock.patch.dict(sys.modules, {"sqlcipher3": fake_sqlcipher3}):
            mod = _fresh_import()
            result = mod.get_sqlcipher_module()
            assert hasattr(result, "dbapi2")
            assert result.dbapi2 is sentinel

    def test_multiple_calls_return_same_module(self):
        """Repeated calls should consistently return the same module."""
        fake_sqlcipher3 = types.ModuleType("sqlcipher3")
        fake_sqlcipher3.dbapi2 = object()

        with mock.patch.dict(sys.modules, {"sqlcipher3": fake_sqlcipher3}):
            mod = _fresh_import()
            r1 = mod.get_sqlcipher_module()
            r2 = mod.get_sqlcipher_module()
            assert r1 is r2

    def test_returns_module_with_connect_attribute(self):
        """Validates that a module with a connect() callable works fine."""
        fake_sqlcipher3 = types.ModuleType("sqlcipher3")
        fake_sqlcipher3.dbapi2 = types.ModuleType("sqlcipher3.dbapi2")
        fake_sqlcipher3.connect = lambda db: None

        with mock.patch.dict(sys.modules, {"sqlcipher3": fake_sqlcipher3}):
            mod = _fresh_import()
            result = mod.get_sqlcipher_module()
            assert callable(result.connect)


# ---------------------------------------------------------------------------
# 2. ImportError path – sqlcipher3 is NOT available
# ---------------------------------------------------------------------------


class TestGetSqlcipherModuleImportError:
    """When sqlcipher3 cannot be imported, get_sqlcipher_module must raise."""

    def _block_sqlcipher3(self):
        """Return a context manager that makes `import sqlcipher3` fail."""
        original_import = (
            __builtins__.__import__
            if hasattr(__builtins__, "__import__")
            else __import__
        )

        def _guarded_import(name, *args, **kwargs):
            if name == "sqlcipher3":
                raise ImportError("No module named 'sqlcipher3'")
            return original_import(name, *args, **kwargs)

        return mock.patch("builtins.__import__", side_effect=_guarded_import)

    def test_raises_import_error(self):
        """An ImportError must be raised when sqlcipher3 is absent."""
        with self._block_sqlcipher3():
            # Remove sqlcipher3 from sys.modules if cached
            with mock.patch.dict(sys.modules, {"sqlcipher3": None}):
                mod = _fresh_import()
                with pytest.raises(ImportError):
                    mod.get_sqlcipher_module()

    def test_error_message_mentions_sqlcipher3(self):
        """The error message should mention sqlcipher3."""
        with self._block_sqlcipher3():
            with mock.patch.dict(sys.modules, {"sqlcipher3": None}):
                mod = _fresh_import()
                with pytest.raises(ImportError, match="sqlcipher3"):
                    mod.get_sqlcipher_module()

    def test_error_message_mentions_pdm_install(self):
        """The error message should mention pdm install."""
        with self._block_sqlcipher3():
            with mock.patch.dict(sys.modules, {"sqlcipher3": None}):
                mod = _fresh_import()
                with pytest.raises(ImportError, match="pdm install"):
                    mod.get_sqlcipher_module()

    def test_error_message_mentions_system_library(self):
        """The error message should mention SQLCipher system library."""
        with self._block_sqlcipher3():
            with mock.patch.dict(sys.modules, {"sqlcipher3": None}):
                mod = _fresh_import()
                with pytest.raises(
                    ImportError, match="SQLCipher system library"
                ):
                    mod.get_sqlcipher_module()


# ---------------------------------------------------------------------------
# 3. Feature detection – module attributes
# ---------------------------------------------------------------------------


class TestModuleAttributes:
    """Verify the public API surface of the compat module."""

    def test_get_sqlcipher_module_is_callable(self):
        """get_sqlcipher_module must be a callable."""
        mod = importlib.import_module(MODULE_PATH)
        assert callable(mod.get_sqlcipher_module)

    def test_module_has_docstring(self):
        """The module should have a docstring describing its purpose."""
        mod = importlib.import_module(MODULE_PATH)
        assert mod.__doc__ is not None
        assert "SQLCipher" in mod.__doc__

    def test_function_has_docstring(self):
        """get_sqlcipher_module should have a docstring."""
        mod = importlib.import_module(MODULE_PATH)
        assert mod.get_sqlcipher_module.__doc__ is not None

    def test_docstring_documents_return_type(self):
        """The function docstring should mention Returns."""
        mod = importlib.import_module(MODULE_PATH)
        assert "Returns" in mod.get_sqlcipher_module.__doc__

    def test_docstring_documents_raises(self):
        """The function docstring should mention Raises / ImportError."""
        mod = importlib.import_module(MODULE_PATH)
        assert "ImportError" in mod.get_sqlcipher_module.__doc__


# ---------------------------------------------------------------------------
# 4. Platform compat – the module works regardless of which package provides
#    the sqlcipher3 namespace (sqlcipher3-binary vs sqlcipher3 source build).
# ---------------------------------------------------------------------------


class TestPlatformCompatibility:
    """Simulates different package backends behind the sqlcipher3 name."""

    def test_binary_package(self):
        """Simulate sqlcipher3-binary providing the sqlcipher3 namespace."""
        fake = types.ModuleType("sqlcipher3")
        fake._binary = True  # marker attribute
        fake.dbapi2 = types.ModuleType("sqlcipher3.dbapi2")

        with mock.patch.dict(sys.modules, {"sqlcipher3": fake}):
            mod = _fresh_import()
            result = mod.get_sqlcipher_module()
            assert result._binary is True

    def test_source_package(self):
        """Simulate sqlcipher3 built from source."""
        fake = types.ModuleType("sqlcipher3")
        fake._source = True
        fake.dbapi2 = types.ModuleType("sqlcipher3.dbapi2")

        with mock.patch.dict(sys.modules, {"sqlcipher3": fake}):
            mod = _fresh_import()
            result = mod.get_sqlcipher_module()
            assert result._source is True

    def test_module_identity_preserved(self):
        """The compat layer should not wrap or proxy the module."""
        fake = types.ModuleType("sqlcipher3")
        fake.dbapi2 = object()

        with mock.patch.dict(sys.modules, {"sqlcipher3": fake}):
            mod = _fresh_import()
            result = mod.get_sqlcipher_module()
            # Must be the exact same object, not a wrapper
            assert result is fake
            assert type(result) is types.ModuleType


# ---------------------------------------------------------------------------
# 5. Edge cases
# ---------------------------------------------------------------------------


class TestEdgeCases:
    """Assorted edge-case scenarios."""

    def test_import_error_is_not_generic_exception(self):
        """The raised exception must be ImportError, not a generic Exception."""
        with mock.patch.dict(sys.modules, {"sqlcipher3": None}):
            mod = _fresh_import()
            with pytest.raises(ImportError) as exc_info:
                mod.get_sqlcipher_module()
            assert type(exc_info.value) is ImportError

    def test_no_side_effects_on_sys_modules(self):
        """Calling get_sqlcipher_module should not add unexpected entries."""
        fake = types.ModuleType("sqlcipher3")
        fake.dbapi2 = object()

        before_keys = set(sys.modules.keys())
        with mock.patch.dict(sys.modules, {"sqlcipher3": fake}):
            mod = _fresh_import()
            mod.get_sqlcipher_module()
        after_keys = set(sys.modules.keys())
        # Only the compat module itself (from reload) may appear; no others
        unexpected = after_keys - before_keys - {MODULE_PATH}
        # Filter out __pycache__ or bytecode artifacts
        unexpected = {k for k in unexpected if "sqlcipher" in k.lower()}
        assert not unexpected, f"Unexpected modules added: {unexpected}"

    def test_reloadable(self):
        """The module should be safely reloadable."""
        fake = types.ModuleType("sqlcipher3")
        fake.dbapi2 = object()

        with mock.patch.dict(sys.modules, {"sqlcipher3": fake}):
            mod = _fresh_import()
            # Reload again
            importlib.reload(mod)
            result = mod.get_sqlcipher_module()
            assert result is fake
