"""Tests for db_utils module."""

import threading
from unittest.mock import Mock, patch, MagicMock

import pytest


class TestGetDbSession:
    """Tests for get_db_session function."""

    def test_raises_error_from_background_thread_without_context(self):
        """Should raise error when called from background thread without app context."""
        from local_deep_research.utilities.db_utils import get_db_session

        error_raised = [False]
        error_message = [""]

        def background_task():
            try:
                get_db_session()
            except RuntimeError as e:
                error_raised[0] = True
                error_message[0] = str(e)

        # Run in a background thread
        thread = threading.Thread(
            target=background_task, name="TestBackgroundThread"
        )
        thread.start()
        thread.join()

        assert error_raised[0], (
            "Should raise RuntimeError from background thread"
        )
        assert "background thread" in error_message[0].lower()

    def test_returns_session_when_username_provided(self):
        """Should return session when username is explicitly provided."""
        from local_deep_research.utilities.db_utils import get_db_session

        mock_session = Mock()

        with patch(
            "local_deep_research.utilities.db_utils.db_manager"
        ) as mock_manager:
            with patch(
                "local_deep_research.utilities.db_utils.has_app_context",
                return_value=True,
            ):
                mock_manager.get_session.return_value = mock_session

                result = get_db_session(username="testuser")

                assert result == mock_session
                mock_manager.get_session.assert_called_with("testuser")

    def test_raises_error_when_no_db_for_user(self):
        """Should raise error when no database found for user."""
        from local_deep_research.utilities.db_utils import get_db_session

        with patch(
            "local_deep_research.utilities.db_utils.db_manager"
        ) as mock_manager:
            with patch(
                "local_deep_research.utilities.db_utils.has_app_context",
                return_value=True,
            ):
                mock_manager.get_session.return_value = None

                with pytest.raises(RuntimeError, match="No database found"):
                    get_db_session(username="nonexistent")

    def test_returns_session_from_flask_g(self):
        """Should return session from Flask g object if present."""
        from local_deep_research.utilities.db_utils import get_db_session

        mock_session = Mock()
        mock_g = Mock()
        mock_g.db_session = mock_session

        with patch(
            "local_deep_research.utilities.db_utils.has_app_context",
            return_value=True,
        ):
            with patch("local_deep_research.utilities.db_utils.g", mock_g):
                result = get_db_session()

                assert result == mock_session

    def test_checks_flask_g_for_session(self):
        """Should check Flask g object for db_session."""
        # This tests that the function checks g.db_session
        from local_deep_research.utilities.db_utils import get_db_session

        mock_session = Mock()
        mock_g = Mock()
        mock_g.db_session = mock_session

        with patch(
            "local_deep_research.utilities.db_utils.has_app_context",
            return_value=True,
        ):
            with patch("local_deep_research.utilities.db_utils.g", mock_g):
                result = get_db_session()
                # Should return a session (the mock)
                assert result is not None
                # Verify g.db_session was accessed
                assert hasattr(mock_g, "db_session")


class TestGetSettingsManager:
    """Tests for get_settings_manager function."""

    def test_returns_settings_manager(self):
        """Should return a SettingsManager instance."""
        from local_deep_research.utilities.db_utils import get_settings_manager

        mock_manager = Mock()

        with patch(
            "local_deep_research.utilities.db_utils.has_app_context",
            return_value=False,
        ):
            with patch(
                "local_deep_research.utilities.db_utils.get_db_session",
                side_effect=RuntimeError("No session"),
            ):
                with patch(
                    "local_deep_research.settings.SettingsManager",
                    return_value=mock_manager,
                ):
                    result = get_settings_manager()

                    # Should return a SettingsManager (mocked)
                    assert result is not None

    def test_uses_provided_db_session(self):
        """Should use provided db_session."""
        from local_deep_research.utilities.db_utils import get_settings_manager

        mock_session = Mock()
        mock_manager = Mock()

        with patch(
            "local_deep_research.settings.SettingsManager",
            return_value=mock_manager,
        ) as MockSettingsManager:
            get_settings_manager(db_session=mock_session)

            MockSettingsManager.assert_called_once_with(mock_session)

    def test_handles_no_session(self):
        """Should handle case when no session available."""
        from local_deep_research.utilities.db_utils import get_settings_manager

        with patch(
            "local_deep_research.utilities.db_utils.has_app_context",
            return_value=False,
        ):
            with patch(
                "local_deep_research.utilities.db_utils.get_db_session",
                side_effect=RuntimeError("No session"),
            ):
                with patch(
                    "local_deep_research.settings.SettingsManager"
                ) as MockSettingsManager:
                    get_settings_manager()

                    # Should be called with None session
                    MockSettingsManager.assert_called_once_with(None)


class TestNoDbSettings:
    """Tests for no_db_settings decorator."""

    def test_disables_db_during_function_execution(self):
        """Should disable DB session during function execution."""
        from local_deep_research.utilities.db_utils import no_db_settings

        mock_session = Mock()
        mock_manager = Mock()
        mock_manager.db_session = mock_session

        session_during_call = []

        @no_db_settings
        def test_func():
            session_during_call.append(mock_manager.db_session)
            return "result"

        with patch(
            "local_deep_research.utilities.db_utils.get_settings_manager",
            return_value=mock_manager,
        ):
            result = test_func()

            assert result == "result"
            assert session_during_call[0] is None
            # Session should be restored after
            assert mock_manager.db_session == mock_session

    def test_restores_db_on_exception(self):
        """Should restore DB session even if function raises."""
        from local_deep_research.utilities.db_utils import no_db_settings

        mock_session = Mock()
        mock_manager = Mock()
        mock_manager.db_session = mock_session

        @no_db_settings
        def failing_func():
            raise ValueError("Test error")

        with patch(
            "local_deep_research.utilities.db_utils.get_settings_manager",
            return_value=mock_manager,
        ):
            with pytest.raises(ValueError):
                failing_func()

            # Session should still be restored
            assert mock_manager.db_session == mock_session

    def test_preserves_function_metadata(self):
        """Should preserve function name and docstring."""
        from local_deep_research.utilities.db_utils import no_db_settings

        @no_db_settings
        def my_function():
            """My docstring."""
            pass

        assert my_function.__name__ == "my_function"
        assert my_function.__doc__ == "My docstring."


class TestGetSettingFromDbMainThread:
    """Tests for get_setting_from_db_main_thread function."""

    def test_returns_default_in_fallback_mode(self):
        """Should return default value when in fallback LLM mode."""
        from local_deep_research.utilities.db_utils import (
            get_setting_from_db_main_thread,
        )

        with patch(
            "local_deep_research.utilities.db_utils.use_fallback_llm",
            return_value=True,
        ):
            result = get_setting_from_db_main_thread(
                "test.key", default_value="default"
            )

            assert result == "default"

    def test_raises_error_from_background_thread(self):
        """Should raise error when called from background thread."""
        from local_deep_research.utilities.db_utils import (
            get_setting_from_db_main_thread,
        )

        error_raised = [False]

        def background_task():
            try:
                with patch(
                    "local_deep_research.utilities.db_utils.use_fallback_llm",
                    return_value=False,
                ):
                    get_setting_from_db_main_thread("test.key")
            except RuntimeError:
                error_raised[0] = True

        thread = threading.Thread(target=background_task, name="BGThread")
        thread.start()
        thread.join()

        assert error_raised[0], (
            "Should raise RuntimeError from background thread"
        )

    def test_returns_default_when_no_session(self):
        """Should return default when no database session available."""
        from local_deep_research.utilities.db_utils import (
            get_setting_from_db_main_thread,
        )

        with patch(
            "local_deep_research.utilities.db_utils.use_fallback_llm",
            return_value=False,
        ):
            with patch(
                "local_deep_research.utilities.db_utils.has_app_context",
                return_value=True,
            ):
                # Simulate get_user_db_session returning None
                with patch(
                    "local_deep_research.database.session_context.get_user_db_session"
                ) as mock_context:
                    mock_cm = MagicMock()
                    mock_cm.__enter__ = Mock(return_value=None)
                    mock_cm.__exit__ = Mock(return_value=None)
                    mock_context.return_value = mock_cm

                    result = get_setting_from_db_main_thread(
                        "test.key", default_value="fallback"
                    )

                    assert result == "fallback"

    def test_returns_default_on_exception(self):
        """Should return default value on exception."""
        from local_deep_research.utilities.db_utils import (
            get_setting_from_db_main_thread,
        )

        with patch(
            "local_deep_research.utilities.db_utils.use_fallback_llm",
            return_value=False,
        ):
            with patch(
                "local_deep_research.utilities.db_utils.has_app_context",
                return_value=True,
            ):
                # Force an exception in the outer try block
                with patch(
                    "local_deep_research.database.session_context.get_user_db_session",
                    side_effect=Exception("DB error"),
                ):
                    result = get_setting_from_db_main_thread(
                        "test.key", default_value="fallback"
                    )

                    assert result == "fallback"


class TestDataDir:
    """Tests for DATA_DIR constant."""

    def test_data_dir_is_set(self):
        """DATA_DIR should be set from get_data_directory."""
        from local_deep_research.utilities.db_utils import DATA_DIR

        assert DATA_DIR is not None


class TestThreadSafety:
    """Tests for thread safety mechanisms."""

    def test_thread_name_included_in_error(self):
        """Error message should include thread name."""
        from local_deep_research.utilities.db_utils import get_db_session

        error_message = [""]

        def background_task():
            try:
                get_db_session()
            except RuntimeError as e:
                error_message[0] = str(e)

        thread = threading.Thread(
            target=background_task, name="CustomThreadName"
        )
        thread.start()
        thread.join()

        assert "CustomThreadName" in error_message[0]

    def test_main_thread_not_blocked_without_context(self):
        """MainThread should not be blocked by thread safety check."""
        # Verify that MainThread can proceed past the thread check
        # even without app context (unlike background threads)
        import threading

        # This test runs in MainThread
        assert threading.current_thread().name == "MainThread"

        # The function should not raise RuntimeError for MainThread
        # (it may fail later for other reasons, but not the thread check)
