# allow: no-sut-import — black-box HTTP test; drives real routes through the Flask test client
"""Tests for search favorites API endpoints in settings_routes.py."""

from unittest.mock import patch, MagicMock


# Settings routes are registered under /settings prefix
SETTINGS_PREFIX = "/settings"


class TestGetSearchFavorites:
    """Tests for GET /settings/api/search-favorites endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.get(f"{SETTINGS_PREFIX}/api/search-favorites")
        assert response.status_code == 401, response.status_code

    def test_returns_empty_list_when_no_favorites(self, authenticated_client):
        """Should return empty list when no favorites are set."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            # Return empty list (no favorites)
            mock_manager.get_setting.return_value = []

            response = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/search-favorites"
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["favorites"] == []

    def test_returns_favorites_list(self, authenticated_client):
        """Should return the list of favorite search engines."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.get_setting.return_value = [
                "searxng",
                "arxiv",
                "brave",
                "serper",
            ]

            response = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/search-favorites"
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["favorites"] == ["searxng", "arxiv", "brave", "serper"]
            mock_manager.get_setting.assert_called_once_with(
                "search.favorites", []
            )

    def test_handles_invalid_favorites_value(self, authenticated_client):
        """Should return empty list when favorites value is not a list."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            # Return non-list value
            mock_manager.get_setting.return_value = "not_a_list"

            response = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/search-favorites"
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["favorites"] == []


class TestUpdateSearchFavorites:
    """Tests for PUT /settings/api/search-favorites endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.put(
            f"{SETTINGS_PREFIX}/api/search-favorites",
            json={"favorites": ["searxng"]},
        )
        assert response.status_code == 401, response.status_code

    def test_requires_json_body(self, authenticated_client):
        """Should require JSON body."""
        response = authenticated_client.put(
            f"{SETTINGS_PREFIX}/api/search-favorites",
            data="not json",
            content_type="text/plain",
        )
        # Flask may return 400 or 500 for invalid content type
        assert response.status_code == 400, response.status_code

    def test_requires_favorites_field(self, authenticated_client):
        """Should require favorites field in request."""
        response = authenticated_client.put(
            f"{SETTINGS_PREFIX}/api/search-favorites",
            json={"other_field": "value"},
        )
        assert response.status_code == 400
        data = response.get_json()
        assert "No favorites provided" in data["error"]

    def test_favorites_must_be_list(self, authenticated_client):
        """Should reject non-list favorites value."""
        response = authenticated_client.put(
            f"{SETTINGS_PREFIX}/api/search-favorites",
            json={"favorites": "not_a_list"},
        )
        assert response.status_code == 400
        data = response.get_json()
        assert "must be a list" in data["error"]

    def test_creates_new_favorites_setting(self, authenticated_client):
        """Should create new favorites setting if none exists."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.set_setting.return_value = True

            response = authenticated_client.put(
                f"{SETTINGS_PREFIX}/api/search-favorites",
                json={"favorites": ["searxng", "arxiv"]},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["favorites"] == ["searxng", "arxiv"]
            assert "successfully" in data["message"]

            # Verify set_setting was called
            mock_manager.set_setting.assert_called_once_with(
                "search.favorites", ["searxng", "arxiv"]
            )

    def test_updates_existing_favorites_setting(self, authenticated_client):
        """Should update existing favorites setting."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.set_setting.return_value = True

            response = authenticated_client.put(
                f"{SETTINGS_PREFIX}/api/search-favorites",
                json={"favorites": ["searxng", "brave"]},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["favorites"] == ["searxng", "brave"]

            # Verify set_setting was called
            mock_manager.set_setting.assert_called_once_with(
                "search.favorites", ["searxng", "brave"]
            )

    def test_accepts_empty_favorites_list(self, authenticated_client):
        """Should accept empty favorites list (clear all favorites)."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.set_setting.return_value = True

            response = authenticated_client.put(
                f"{SETTINGS_PREFIX}/api/search-favorites",
                json={"favorites": []},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["favorites"] == []


class TestToggleSearchFavorite:
    """Tests for POST /settings/api/search-favorites/toggle endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.post(
            f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
            json={"engine_id": "searxng"},
        )
        assert response.status_code == 401, response.status_code

    def test_requires_json_body(self, authenticated_client):
        """Should require JSON body."""
        response = authenticated_client.post(
            f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
            data="not json",
            content_type="text/plain",
        )
        # Flask may return 400 or 500 for invalid content type
        assert response.status_code == 400, response.status_code

    def test_requires_engine_id(self, authenticated_client):
        """Should require engine_id field."""
        response = authenticated_client.post(
            f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
            json={"other_field": "value"},
        )
        assert response.status_code == 400
        data = response.get_json()
        assert "No engine_id provided" in data["error"]

    def test_adds_engine_to_favorites(self, authenticated_client):
        """Should add engine to favorites when not already a favorite."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            # No existing favorites
            mock_manager.get_setting.return_value = []
            mock_manager.set_setting.return_value = True

            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": "searxng"},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["engine_id"] == "searxng"
            assert data["is_favorite"] is True
            assert "searxng" in data["favorites"]

    def test_removes_engine_from_favorites(self, authenticated_client):
        """Should remove engine from favorites when already a favorite."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            # Existing favorites including the engine
            mock_manager.get_setting.return_value = [
                "searxng",
                "arxiv",
                "brave",
            ]
            mock_manager.set_setting.return_value = True

            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": "arxiv"},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["engine_id"] == "arxiv"
            assert data["is_favorite"] is False
            assert "arxiv" not in data["favorites"]
            assert "searxng" in data["favorites"]
            assert "brave" in data["favorites"]

    def test_toggle_creates_setting_if_not_exists(self, authenticated_client):
        """Should create favorites setting if it doesn't exist."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            # No existing favorites - SettingsManager returns default
            mock_manager.get_setting.return_value = []
            mock_manager.set_setting.return_value = True

            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": "brave"},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["is_favorite"] is True
            assert data["favorites"] == ["brave"]

            # Verify set_setting was called
            mock_manager.set_setting.assert_called_once_with(
                "search.favorites", ["brave"]
            )


class TestAvailableSearchEnginesWithFavorites:
    """Tests for favorites in GET /settings/api/available-search-engines endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.get(f"{SETTINGS_PREFIX}/api/available-search-engines")
        assert response.status_code == 401, response.status_code

    def test_response_includes_favorites_field(self, authenticated_client):
        """Should include favorites field in response."""
        response = authenticated_client.get(
            f"{SETTINGS_PREFIX}/api/available-search-engines"
        )

        assert response.status_code == 200
        data = response.get_json()

        # Check response structure includes favorites
        assert "favorites" in data
        assert isinstance(data["favorites"], list)

    def test_engine_options_include_is_favorite_field(
        self, authenticated_client
    ):
        """Should include is_favorite field for each engine option."""
        response = authenticated_client.get(
            f"{SETTINGS_PREFIX}/api/available-search-engines"
        )

        assert response.status_code == 200
        data = response.get_json()

        # Check engine_options have is_favorite field
        assert "engine_options" in data
        if data["engine_options"]:
            for option in data["engine_options"]:
                assert "is_favorite" in option
                assert isinstance(option["is_favorite"], bool)

    def test_favorites_workflow_with_available_engines(
        self, authenticated_client
    ):
        """Test that favoriting an engine shows up in available-search-engines."""
        favorites_state = []

        def mock_get_setting(key, default=None):
            if key == "search.favorites":
                return list(favorites_state)
            return default

        def mock_set_setting(key, value):
            nonlocal favorites_state
            if key == "search.favorites":
                favorites_state = list(value)
            return True

        mock_engines = {
            "brave": {
                "display_name": "Brave",
                "module_path": ".engines.search_engine_brave",
                "class_name": "BraveSearchEngine",
            },
            "arxiv": {
                "display_name": "ArXiv",
                "module_path": ".engines.search_engine_arxiv",
                "class_name": "ArxivSearchEngine",
            },
        }

        mock_db_session = MagicMock()

        with (
            patch(
                "local_deep_research.web.utils.route_decorators.SettingsManager"
            ) as mock_manager_class,
            patch(
                "local_deep_research.web_search_engines.search_engines_config.search_config",
                return_value=mock_engines,
            ),
            patch(
                "local_deep_research.web.utils.route_decorators.get_user_db_session",
            ) as mock_session_ctx,
        ):
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_db_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=False
            )
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.get_setting.side_effect = mock_get_setting
            mock_manager.set_setting.side_effect = mock_set_setting

            # Get available engines — no favorites yet
            response = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/available-search-engines"
            )
            assert response.status_code == 200
            data = response.get_json()

            assert isinstance(data["favorites"], list)
            assert len(data["favorites"]) == 0
            assert len(data["engine_options"]) == 2

            # Pick the first engine to favorite
            engine_to_favorite = data["engine_options"][0]["value"]

            # Add it as favorite
            toggle_response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": engine_to_favorite},
            )
            assert toggle_response.status_code == 200
            toggle_data = toggle_response.get_json()
            assert toggle_data["is_favorite"] is True

            # Verify it shows up in available-search-engines
            response2 = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/available-search-engines"
            )
            assert response2.status_code == 200
            data2 = response2.get_json()

            # The engine should now be in favorites
            assert engine_to_favorite in data2["favorites"]

            # The engine should be marked as favorite in options
            for option in data2["engine_options"]:
                if option["value"] == engine_to_favorite:
                    assert option["is_favorite"] is True
                    break


class TestSearchFavoritesIntegration:
    """Integration tests for search favorites workflow."""

    def test_full_favorites_workflow(self, authenticated_client):
        """Test complete workflow: add, get, remove favorites."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager

            # Track favorites state for simulating stateful behavior
            favorites_state = []

            def mock_get_setting(key, default=None):
                if key == "search.favorites":
                    return list(favorites_state)
                return default

            def mock_set_setting(key, value):
                nonlocal favorites_state
                if key == "search.favorites":
                    favorites_state = list(value)
                return True

            mock_manager.get_setting.side_effect = mock_get_setting
            mock_manager.set_setting.side_effect = mock_set_setting

            # Step 1: Get empty favorites
            response = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/search-favorites"
            )
            assert response.status_code == 200
            assert response.get_json()["favorites"] == []

            # Step 2: Add searxng to favorites
            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": "searxng"},
            )
            assert response.status_code == 200
            data = response.get_json()
            assert data["is_favorite"] is True

            # Step 3: Add arxiv to favorites
            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": "arxiv"},
            )
            assert response.status_code == 200
            data = response.get_json()
            assert data["is_favorite"] is True
            assert set(data["favorites"]) == {"searxng", "arxiv"}

            # Step 4: Remove searxng from favorites
            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": "searxng"},
            )
            assert response.status_code == 200
            data = response.get_json()
            assert data["is_favorite"] is False
            assert data["favorites"] == ["arxiv"]

    def test_bulk_update_favorites(self, authenticated_client):
        """Test updating all favorites at once via PUT."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.set_setting.return_value = True

            # Bulk update to new favorites
            response = authenticated_client.put(
                f"{SETTINGS_PREFIX}/api/search-favorites",
                json={"favorites": ["searxng", "arxiv", "brave", "serper"]},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert set(data["favorites"]) == {
                "searxng",
                "arxiv",
                "brave",
                "serper",
            }

            # Verify set_setting was called with the new favorites
            mock_manager.set_setting.assert_called_once_with(
                "search.favorites",
                ["searxng", "arxiv", "brave", "serper"],
            )


class TestSearchFavoritesErrorHandling:
    """Tests for error handling in search favorites endpoints."""

    def test_get_favorites_handles_db_error(self, authenticated_client):
        """DB session errors return 500 JSON via the decorator's error handling."""
        with patch(
            "local_deep_research.web.utils.route_decorators.get_user_db_session"
        ) as mock_session_ctx:
            mock_session_ctx.return_value.__enter__ = MagicMock(
                side_effect=Exception("Database connection failed")
            )

            resp = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/search-favorites"
            )
            assert resp.status_code == 500
            assert resp.get_json()["error"] == "Database session unavailable"

    def test_put_favorites_handles_db_error(self, authenticated_client):
        """DB session errors return 500 JSON via the decorator's error handling."""
        with patch(
            "local_deep_research.web.utils.route_decorators.get_user_db_session"
        ) as mock_session_ctx:
            mock_session_ctx.return_value.__enter__ = MagicMock(
                side_effect=Exception("Database connection failed")
            )

            resp = authenticated_client.put(
                f"{SETTINGS_PREFIX}/api/search-favorites",
                json={"favorites": ["searxng"]},
            )
            assert resp.status_code == 500
            assert resp.get_json()["error"] == "Database session unavailable"

    def test_toggle_favorites_handles_db_error(self, authenticated_client):
        """DB session errors return 500 JSON via the decorator's error handling."""
        with patch(
            "local_deep_research.web.utils.route_decorators.get_user_db_session"
        ) as mock_session_ctx:
            mock_session_ctx.return_value.__enter__ = MagicMock(
                side_effect=Exception("Database connection failed")
            )

            resp = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": "searxng"},
            )
            assert resp.status_code == 500
            assert resp.get_json()["error"] == "Database session unavailable"


class TestSearchFavoritesSettingsManagerFailures:
    """Tests for SettingsManager failure scenarios."""

    def test_update_favorites_handles_set_setting_failure(
        self, authenticated_client
    ):
        """Should return 500 when SettingsManager.set_setting returns False."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            # Simulate set_setting failure
            mock_manager.set_setting.return_value = False

            response = authenticated_client.put(
                f"{SETTINGS_PREFIX}/api/search-favorites",
                json={"favorites": ["searxng"]},
            )

            assert response.status_code == 500
            data = response.get_json()
            assert "error" in data

    def test_toggle_favorites_handles_set_setting_failure(
        self, authenticated_client
    ):
        """Should return 500 when SettingsManager.set_setting returns False during toggle."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.get_setting.return_value = []
            # Simulate set_setting failure
            mock_manager.set_setting.return_value = False

            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": "searxng"},
            )

            assert response.status_code == 500
            data = response.get_json()
            assert "error" in data


class TestSearchFavoritesEdgeCases:
    """Tests for edge cases in search favorites functionality."""

    def test_toggle_with_empty_string_engine_id(self, authenticated_client):
        """Should reject empty string engine_id."""
        response = authenticated_client.post(
            f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
            json={"engine_id": ""},
        )
        assert response.status_code == 400
        data = response.get_json()
        assert "No engine_id provided" in data["error"]

    def test_favorites_preserves_order(self, authenticated_client):
        """Should preserve the order of favorites."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.set_setting.return_value = True

            ordered_favorites = ["zeta", "alpha", "beta", "gamma"]
            response = authenticated_client.put(
                f"{SETTINGS_PREFIX}/api/search-favorites",
                json={"favorites": ordered_favorites},
            )

            assert response.status_code == 200
            data = response.get_json()
            # Order should be preserved
            assert data["favorites"] == ordered_favorites

    def test_toggle_does_not_create_duplicates(self, authenticated_client):
        """Should not create duplicate entries when toggling."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            # Favorites already contains the engine
            mock_manager.get_setting.return_value = ["searxng", "arxiv"]
            mock_manager.set_setting.return_value = True

            # Toggle searxng (should remove it, not add duplicate)
            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": "searxng"},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["is_favorite"] is False
            # Verify set_setting was called with searxng removed
            mock_manager.set_setting.assert_called_once()
            call_args = mock_manager.set_setting.call_args[0]
            assert call_args[1] == ["arxiv"]

    def test_favorites_with_special_characters(self, authenticated_client):
        """Should handle engine IDs with special characters."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.set_setting.return_value = True

            special_favorites = [
                "engine-with-dash",
                "engine_with_underscore",
                "engine.with.dots",
            ]
            response = authenticated_client.put(
                f"{SETTINGS_PREFIX}/api/search-favorites",
                json={"favorites": special_favorites},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["favorites"] == special_favorites

    def test_toggle_nonexistent_engine_id(self, authenticated_client):
        """Should allow favoriting engine IDs that may not exist yet."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.get_setting.return_value = []
            mock_manager.set_setting.return_value = True

            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": "nonexistent_engine_12345"},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["is_favorite"] is True
            assert "nonexistent_engine_12345" in data["favorites"]

    def test_update_with_duplicate_entries(self, authenticated_client):
        """Should accept list with duplicates (validation is caller's responsibility)."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.set_setting.return_value = True

            # API accepts duplicates - deduplication is not enforced at API level
            response = authenticated_client.put(
                f"{SETTINGS_PREFIX}/api/search-favorites",
                json={"favorites": ["searxng", "searxng", "arxiv"]},
            )

            assert response.status_code == 200

    def test_update_with_large_favorites_list(self, authenticated_client):
        """Should handle large favorites lists."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.set_setting.return_value = True

            large_favorites = [f"engine_{i}" for i in range(100)]
            response = authenticated_client.put(
                f"{SETTINGS_PREFIX}/api/search-favorites",
                json={"favorites": large_favorites},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert len(data["favorites"]) == 100


class TestAvailableSearchEnginesFavoritesSorting:
    """Tests for sorting behavior in available search engines endpoint."""

    def test_favorites_appear_first_in_engine_options(
        self, authenticated_client
    ):
        """Favorites should be sorted to appear first in engine_options."""
        # This is an integration test using real data
        response = authenticated_client.get(
            f"{SETTINGS_PREFIX}/api/available-search-engines"
        )

        assert response.status_code == 200
        data = response.get_json()

        # Verify favorites are sorted first
        if data["engine_options"]:
            favorite_indices = []
            non_favorite_indices = []
            for i, opt in enumerate(data["engine_options"]):
                if opt.get("is_favorite"):
                    favorite_indices.append(i)
                else:
                    non_favorite_indices.append(i)

            # All favorite indices should be less than non-favorite indices
            if favorite_indices and non_favorite_indices:
                assert max(favorite_indices) < min(non_favorite_indices), (
                    "Favorites should appear before non-favorites"
                )

    def test_engines_dict_includes_is_favorite_field(
        self, authenticated_client
    ):
        """Each engine in engines dict should have is_favorite field."""
        response = authenticated_client.get(
            f"{SETTINGS_PREFIX}/api/available-search-engines"
        )

        assert response.status_code == 200
        data = response.get_json()

        # Check engines dict has is_favorite for each engine
        if data.get("engines"):
            for engine_id, engine_data in data["engines"].items():
                assert "is_favorite" in engine_data, (
                    f"Engine {engine_id} missing is_favorite field"
                )
                assert isinstance(engine_data["is_favorite"], bool), (
                    f"Engine {engine_id} is_favorite should be boolean"
                )

    def test_favoriting_moves_engine_to_top(self, authenticated_client):
        """When an engine is favorited, it should move to the top of the list."""
        favorites_state = []

        def mock_get_setting(key, default=None):
            if key == "search.favorites":
                return list(favorites_state)
            return default

        def mock_set_setting(key, value):
            nonlocal favorites_state
            if key == "search.favorites":
                favorites_state = list(value)
            return True

        mock_engines = {
            "alpha": {
                "display_name": "Alpha",
                "module_path": ".engines.search_engine_alpha",
                "class_name": "AlphaSearchEngine",
            },
            "beta": {
                "display_name": "Beta",
                "module_path": ".engines.search_engine_beta",
                "class_name": "BetaSearchEngine",
            },
            "gamma": {
                "display_name": "Gamma",
                "module_path": ".engines.search_engine_gamma",
                "class_name": "GammaSearchEngine",
            },
        }

        mock_db_session = MagicMock()

        with (
            patch(
                "local_deep_research.web.utils.route_decorators.SettingsManager"
            ) as mock_manager_class,
            patch(
                "local_deep_research.web_search_engines.search_engines_config.search_config",
                return_value=mock_engines,
            ),
            patch(
                "local_deep_research.web.utils.route_decorators.get_user_db_session",
            ) as mock_session_ctx,
        ):
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_db_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=False
            )
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            mock_manager.get_setting.side_effect = mock_get_setting
            mock_manager.set_setting.side_effect = mock_set_setting

            # Get initial list — no favorites, all engines present
            response = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/available-search-engines"
            )
            assert response.status_code == 200
            data = response.get_json()
            assert len(data["engine_options"]) == 3

            # Pick the last engine alphabetically to favorite
            engine_to_favorite = "gamma"

            # Favorite it
            toggle_response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": engine_to_favorite},
            )
            assert toggle_response.status_code == 200

            # Verify engine is now at the top with other favorites
            response2 = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/available-search-engines"
            )
            assert response2.status_code == 200
            data2 = response2.get_json()

            # Find the newly favorited engine's index
            engine_index = None
            for i, opt in enumerate(data2["engine_options"]):
                if opt["value"] == engine_to_favorite:
                    engine_index = i
                    assert opt["is_favorite"] is True
                    break

            assert engine_index is not None, "Engine should still be in list"

            # Verify no non-favorites appear before it
            for i in range(engine_index):
                assert data2["engine_options"][i].get("is_favorite"), (
                    f"Non-favorite at index {i} appears before favorited engine"
                )


class TestSearchFavoritesNullHandling:
    """Tests for null/None value handling."""

    def test_get_favorites_with_none_from_settings_manager(
        self, authenticated_client
    ):
        """Should handle None returned from SettingsManager gracefully."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            # Return None instead of a list
            mock_manager.get_setting.return_value = None

            response = authenticated_client.get(
                f"{SETTINGS_PREFIX}/api/search-favorites"
            )

            assert response.status_code == 200
            data = response.get_json()
            # Should return empty list for None
            assert data["favorites"] == []

    def test_toggle_with_none_from_settings_manager(self, authenticated_client):
        """Should handle None favorites from SettingsManager during toggle."""
        with patch(
            "local_deep_research.web.utils.route_decorators.SettingsManager"
        ) as mock_manager_class:
            mock_manager = MagicMock()
            mock_manager_class.return_value = mock_manager
            # Return None (simulating setting doesn't exist)
            mock_manager.get_setting.return_value = None
            mock_manager.set_setting.return_value = True

            response = authenticated_client.post(
                f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                json={"engine_id": "searxng"},
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["is_favorite"] is True
            assert data["favorites"] == ["searxng"]

    def test_update_favorites_rejects_null_value(self, authenticated_client):
        """Should reject null as favorites value."""
        response = authenticated_client.put(
            f"{SETTINGS_PREFIX}/api/search-favorites",
            json={"favorites": None},
        )

        assert response.status_code == 400
        data = response.get_json()
        # None is treated as "not provided"
        assert "No favorites provided" in data["error"]
