"""
Deep coverage tests for settings_routes.py targeting ~170 remaining uncovered statements.

Uncovered functions/branches targeted:
- _get_setting_from_session: db_session is None
- save_all_settings: corrupted llm.provider, bracket chars, unknown prefix,
  single-update non-bool message, key not in original_values, set_setting fails,
  new setting UI detection (bool/int/dict), skip empty keys, validation error
- save_settings: commit failure rollback, failed_count flash, non-editable POST filter,
  setting exception in loop
- api_get_all_settings: exception path
- api_get_db_setting: exception path
- api_update_setting: exception path
- api_delete_setting: exception path
- api_import_settings: exception path
- api_get_categories: exception path
- api_get_search_favorites: non-list reset, exception path
- api_update_search_favorites: exception path
- api_toggle_search_favorite: exception path
- fix_corrupted_settings: llm.temperature, llm.max_tokens, search sub-keys,
  report.searches_per_section, app sub-keys, report unknown key fallback,
  empty dict corruption
- inject_csrf_token: context processor
- save_all_settings: database/app prefix categorization
"""

from contextlib import contextmanager
from unittest.mock import MagicMock, Mock, patch

from flask import Flask, jsonify

from local_deep_research.web.auth.routes import auth_bp
from local_deep_research.web.routes.settings_routes import settings_bp

# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------

MODULE = "local_deep_research.web.routes.settings_routes"
DECORATOR_MODULE = "local_deep_research.web.utils.route_decorators"
SETTINGS_PREFIX = "/settings"


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


def _make_setting(
    key="test.key",
    value="val",
    ui_element="text",
    name="Test Key",
    description="desc",
    category="general",
    setting_type="app",
    editable=True,
    visible=True,
    options=None,
    min_value=None,
    max_value=None,
    step=None,
    updated_at=None,
):
    """Build a mock Setting ORM object."""
    s = MagicMock()
    s.key = key
    s.value = value
    s.ui_element = ui_element
    s.name = name
    s.description = description
    s.category = category
    s.type = setting_type
    s.editable = editable
    s.visible = visible
    s.options = options
    s.min_value = min_value
    s.max_value = max_value
    s.step = step
    s.updated_at = updated_at
    return s


def _create_test_app():
    """Create a minimal Flask app with only the settings blueprint."""
    app = Flask(__name__)
    app.config["SECRET_KEY"] = "test-secret"
    app.config["WTF_CSRF_ENABLED"] = False
    app.register_blueprint(auth_bp)
    app.register_blueprint(settings_bp)

    @app.errorhandler(500)
    def _handle_500(error):
        return jsonify({"error": "Internal server error"}), 500

    return app


@contextmanager
def _authenticated_client(app):
    """Provide an authenticated test client with mocked auth and settings_limit."""
    mock_db = Mock()
    mock_db.connections = {"testuser": True}
    mock_db.has_encryption = False

    @contextmanager
    def _fake_session(*args, **kwargs):
        yield MagicMock()

    patches = [
        patch("local_deep_research.web.auth.decorators.db_manager", mock_db),
        patch(
            f"{DECORATOR_MODULE}.get_user_db_session", side_effect=_fake_session
        ),
        patch(f"{MODULE}.settings_limit", lambda f: f),
    ]

    started = []
    try:
        for p in patches:
            started.append(p.start())
        with app.test_client() as client:
            with client.session_transaction() as sess:
                sess["username"] = "testuser"
                sess["session_id"] = "test-session-id"
            yield client
    finally:
        for p in patches:
            p.stop()


# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------


class TestGetSettingFromSession:
    """_get_setting_from_session: db_session is None branch."""

    def test_returns_default_when_db_session_is_none(self):
        from local_deep_research.web.routes.settings_routes import (
            _get_setting_from_session,
        )

        @contextmanager
        def _fake_ctx(*a, **kw):
            yield None

        app = _create_test_app()
        with app.test_request_context():
            from flask import session

            session["username"] = "testuser"
            with patch(f"{MODULE}.get_user_db_session", side_effect=_fake_ctx):
                result = _get_setting_from_session("any.key", "fallback")
        assert result == "fallback"


class TestInjectCsrfToken:
    """Test the inject_csrf_token context processor."""

    def test_injects_callable(self):
        from local_deep_research.web.routes.settings_routes import (
            inject_csrf_token,
        )

        result = inject_csrf_token()
        assert "csrf_token" in result
        assert callable(result["csrf_token"])


class TestSaveAllSettingsNewSettingUIDetection:
    """save_all_settings: new setting creation with different value types."""

    @patch(f"{MODULE}.create_or_update_setting")
    def test_new_bool_setting_gets_checkbox(self, mock_create):
        mock_new = _make_setting(key="app.flag", value=True)
        mock_new.type = "app"
        mock_create.return_value = mock_new

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [[], []]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"app.flag": True},
                )
        assert resp.status_code == 200
        call_args = mock_create.call_args[0][0]
        assert call_args["ui_element"] == "checkbox"

    @patch(f"{MODULE}.create_or_update_setting")
    def test_new_int_setting_gets_number(self, mock_create):
        mock_new = _make_setting(key="app.count", value=42)
        mock_new.type = "app"
        mock_create.return_value = mock_new

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [[], []]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"app.count": 42},
                )
        assert resp.status_code == 200
        call_args = mock_create.call_args[0][0]
        assert call_args["ui_element"] == "number"

    @patch(f"{MODULE}.create_or_update_setting")
    def test_new_dict_setting_gets_textarea(self, mock_create):
        mock_new = _make_setting(key="report.structure", value={})
        mock_new.type = "report"
        mock_create.return_value = mock_new

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [[], []]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"report.structure": {"a": 1}},
                )
        assert resp.status_code == 200
        call_args = mock_create.call_args[0][0]
        assert call_args["ui_element"] == "textarea"

    @patch(f"{MODULE}.create_or_update_setting")
    def test_unknown_prefix_rejected_with_validation_error(self, mock_create):
        """Unknown prefix is rejected by the namespace gate with 400."""
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [[], []]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"custom.param": "value"},
                )
        # Namespace gate rejects the key; response has status=error with 400
        assert resp.status_code == 400
        data = resp.get_json()
        assert any(
            e["key"] == "custom.param" and "not allowed" in e["error"]
            for e in data["errors"]
        )
        mock_create.assert_not_called()


class TestSaveAllSettingsSkipInvalidKeys:
    """save_all_settings: empty string keys are skipped."""

    @patch(f"{MODULE}.set_setting", return_value=True)
    @patch(
        f"{MODULE}.coerce_setting_for_write",
        side_effect=lambda key, value, ui_element: value,
    )
    @patch(f"{MODULE}.validate_setting", return_value=(True, None))
    def test_empty_key_skipped(self, mock_v, mock_c, mock_s):
        setting = _make_setting(key="llm.model", value="gpt-4", editable=True)
        setting.type = "llm"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"": "ignored", "llm.model": "gpt-4"},
                )
        assert resp.status_code == 200
        assert mock_s.call_count == 1


class TestSaveAllSettingsCorruptedBranches:
    """save_all_settings: additional corrupted value correction branches."""

    @patch(f"{MODULE}.set_setting", return_value=True)
    @patch(
        f"{MODULE}.coerce_setting_for_write",
        side_effect=lambda key, value, ui_element: value,
    )
    @patch(f"{MODULE}.validate_setting", return_value=(True, None))
    def test_corrupted_llm_provider(self, mock_v, mock_c, mock_s):
        setting = _make_setting(
            key="llm.provider", value="openai", ui_element="text", editable=True
        )
        setting.type = "llm"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"llm.provider": "[object Object]"},
                )
        assert resp.status_code == 200

    @patch(f"{MODULE}.set_setting", return_value=True)
    @patch(
        f"{MODULE}.coerce_setting_for_write",
        side_effect=lambda key, value, ui_element: value,
    )
    @patch(f"{MODULE}.validate_setting", return_value=(True, None))
    def test_corrupted_unknown_key_becomes_none(self, mock_v, mock_c, mock_s):
        setting = _make_setting(
            key="database.name", value="test", ui_element="text", editable=True
        )
        setting.type = "database"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"database.name": "[object Object]"},
                )
        assert resp.status_code == 200

    @patch(f"{MODULE}.set_setting", return_value=True)
    @patch(
        f"{MODULE}.coerce_setting_for_write",
        side_effect=lambda key, value, ui_element: value,
    )
    @patch(f"{MODULE}.validate_setting", return_value=(True, None))
    def test_corrupted_bracket_char(self, mock_v, mock_c, mock_s):
        """Value '{' (single bracket) is detected as corrupted."""
        setting = _make_setting(
            key="llm.model", value="gpt-4", ui_element="text", editable=True
        )
        setting.type = "llm"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"llm.model": "{"},
                )
        assert resp.status_code == 200


class TestSaveAllSettingsSuccessMessages:
    """save_all_settings: different success message formats."""

    @patch(f"{MODULE}.set_setting", return_value=True)
    @patch(
        f"{MODULE}.coerce_setting_for_write",
        side_effect=lambda key, value, ui_element: value,
    )
    @patch(f"{MODULE}.validate_setting", return_value=(True, None))
    def test_single_bool_enabled_message(self, mock_v, mock_c, mock_s):
        setting = _make_setting(
            key="app.dark_mode",
            value=True,
            ui_element="checkbox",
            editable=True,
        )
        setting.type = "app"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"app.dark_mode": True},
                )
        assert resp.status_code == 200
        data = resp.get_json()
        assert "enabled" in data["message"] or "disabled" in data["message"]

    @patch(f"{MODULE}.set_setting", return_value=True)
    @patch(
        f"{MODULE}.coerce_setting_for_write",
        side_effect=lambda key, value, ui_element: value,
    )
    @patch(f"{MODULE}.validate_setting", return_value=(True, None))
    def test_single_non_bool_updated_message(self, mock_v, mock_c, mock_s):
        setting = _make_setting(
            key="llm.temperature", value=0.7, ui_element="number", editable=True
        )
        setting.type = "llm"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"llm.temperature": 0.5},
                )
        assert resp.status_code == 200
        data = resp.get_json()
        assert "updated" in data["message"]


class TestSaveAllSettingsValidationError:
    """save_all_settings: validation failure returns 400."""

    @patch(f"{MODULE}.coerce_setting_for_write", return_value=-1)
    @patch(
        f"{MODULE}.validate_setting",
        return_value=(False, "Value must be at least 0"),
    )
    def test_validation_error_on_existing(self, mock_v, mock_c):
        setting = _make_setting(
            key="search.iterations",
            value=3,
            ui_element="number",
            editable=True,
            name="Iterations",
        )
        setting.type = "search"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"search.iterations": -1},
                )
        assert resp.status_code == 400
        data = resp.get_json()
        assert data["status"] == "error"
        assert len(data["errors"]) == 1


class TestSaveSettingsFormPost:
    """save_settings: traditional POST edge cases."""

    def test_commit_failure_rollback(self):
        setting = _make_setting(
            key="llm.model", value="gpt-4", ui_element="text", editable=True
        )

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.return_value = [setting]
                mock_session.commit.side_effect = RuntimeError("commit failed")
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                with patch(
                    f"{DECORATOR_MODULE}.SettingsManager"
                ) as mock_sm_cls:
                    mock_sm = MagicMock()
                    mock_sm.set_setting.return_value = True
                    mock_sm_cls.return_value = mock_sm
                    with patch(
                        f"{MODULE}.coerce_setting_for_write",
                        return_value="gpt-4",
                    ):
                        resp = client.post(
                            f"{SETTINGS_PREFIX}/save_settings",
                            data={"llm.model": "gpt-4"},
                        )
        assert resp.status_code == 302
        mock_session.rollback.assert_called_once()

    def test_set_setting_returns_false(self):
        setting = _make_setting(
            key="llm.model", value="gpt-4", ui_element="text", editable=True
        )

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.return_value = [setting]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                with patch(
                    f"{DECORATOR_MODULE}.SettingsManager"
                ) as mock_sm_cls:
                    mock_sm = MagicMock()
                    mock_sm.set_setting.return_value = False
                    mock_sm_cls.return_value = mock_sm
                    with patch(
                        f"{MODULE}.coerce_setting_for_write",
                        return_value="gpt-4",
                    ):
                        resp = client.post(
                            f"{SETTINGS_PREFIX}/save_settings",
                            data={"llm.model": "gpt-4"},
                        )
        assert resp.status_code == 302

    def test_setting_exception_in_loop(self):
        setting = _make_setting(
            key="llm.model", value="gpt-4", ui_element="text", editable=True
        )

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.return_value = [setting]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                with patch(
                    f"{DECORATOR_MODULE}.SettingsManager"
                ) as mock_sm_cls:
                    mock_sm = MagicMock()
                    mock_sm.set_setting.side_effect = RuntimeError("unexpected")
                    mock_sm_cls.return_value = mock_sm
                    with patch(
                        f"{MODULE}.coerce_setting_for_write",
                        return_value="gpt-4",
                    ):
                        resp = client.post(
                            f"{SETTINGS_PREFIX}/save_settings",
                            data={"llm.model": "gpt-4"},
                        )
        assert resp.status_code == 302

    def test_non_editable_skipped(self):
        setting = _make_setting(
            key="app.locked", value="v", ui_element="text", editable=False
        )

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.return_value = [setting]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                with patch(
                    f"{DECORATOR_MODULE}.SettingsManager"
                ) as mock_sm_cls:
                    mock_sm = MagicMock()
                    mock_sm.set_setting.return_value = True
                    mock_sm_cls.return_value = mock_sm
                    resp = client.post(
                        f"{SETTINGS_PREFIX}/save_settings",
                        data={"app.locked": "new_val"},
                    )
        assert resp.status_code == 302


class TestApiGetAllSettingsException:
    """api_get_all_settings: exception -> 500."""

    def test_exception_returns_500(self):
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_ctx.side_effect = RuntimeError("db fail")
                resp = client.get(f"{SETTINGS_PREFIX}/api")
        assert resp.status_code == 500
        assert "error" in resp.get_json()


class TestApiGetDbSettingException:
    """api_get_db_setting: exception -> 500."""

    def test_exception_returns_500(self):
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_ctx.side_effect = RuntimeError("db fail")
                resp = client.get(f"{SETTINGS_PREFIX}/api/llm.model")
        assert resp.status_code == 500


class TestApiUpdateSettingException:
    """api_update_setting: unhandled exception -> 500."""

    def test_exception_returns_500(self):
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_ctx.side_effect = RuntimeError("db fail")
                resp = client.put(
                    f"{SETTINGS_PREFIX}/api/llm.model",
                    json={"value": "test"},
                )
        assert resp.status_code == 500
        assert "error" in resp.get_json()


class TestApiDeleteSettingException:
    """api_delete_setting: unhandled exception -> 500."""

    def test_exception_returns_500(self):
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_ctx.side_effect = RuntimeError("db fail")
                resp = client.delete(f"{SETTINGS_PREFIX}/api/llm.model")
        assert resp.status_code == 500
        assert "error" in resp.get_json()


class TestApiGetCategoriesException:
    """api_get_categories: exception -> 500."""

    def test_exception_returns_500(self):
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_ctx.side_effect = RuntimeError("db fail")
                resp = client.get(f"{SETTINGS_PREFIX}/api/categories")
        assert resp.status_code == 500


class TestApiImportSettingsException:
    """api_import_settings: exception -> 500."""

    def test_exception_returns_500(self):
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_ctx.side_effect = RuntimeError("db fail")
                resp = client.post(f"{SETTINGS_PREFIX}/api/import")
        assert resp.status_code == 500


class TestApiGetSearchFavoritesEdge:
    """api_get_search_favorites: edge cases."""

    def test_non_list_favorites_reset(self):
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                with patch(
                    f"{DECORATOR_MODULE}.SettingsManager"
                ) as mock_sm_cls:
                    mock_sm = MagicMock()
                    mock_sm.get_setting.return_value = "not_a_list"
                    mock_sm_cls.return_value = mock_sm
                    resp = client.get(f"{SETTINGS_PREFIX}/api/search-favorites")
        assert resp.status_code == 200
        assert resp.get_json()["favorites"] == []

    def test_exception_returns_500(self):
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_ctx.side_effect = RuntimeError("fail")
                resp = client.get(f"{SETTINGS_PREFIX}/api/search-favorites")
        assert resp.status_code == 500


class TestApiUpdateSearchFavoritesException:
    """api_update_search_favorites: exception -> 500."""

    def test_exception_returns_500(self):
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_ctx.side_effect = RuntimeError("fail")
                resp = client.put(
                    f"{SETTINGS_PREFIX}/api/search-favorites",
                    json={"favorites": ["google"]},
                )
        assert resp.status_code == 500


class TestApiToggleSearchFavoriteException:
    """api_toggle_search_favorite: exception -> 500."""

    def test_exception_returns_500(self):
        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_ctx.side_effect = RuntimeError("fail")
                resp = client.post(
                    f"{SETTINGS_PREFIX}/api/search-favorites/toggle",
                    json={"engine_id": "google"},
                )
        assert resp.status_code == 500


class TestFixCorruptedSettingsSubKeys:
    """fix_corrupted_settings: additional corrupted key defaults."""

    def _post_fix(self, client, settings_list):
        with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
            mock_session = MagicMock()
            mock_session.query.return_value.group_by.return_value.having.return_value.all.return_value = []
            mock_session.query.return_value.all.return_value = settings_list
            mock_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
            return client.post(f"{SETTINGS_PREFIX}/fix_corrupted_settings")

    def test_fixes_search_sub_keys(self):
        settings = [
            _make_setting(key="search.questions_per_iteration", value=None),
            _make_setting(key="search.searches_per_section", value="null"),
            _make_setting(
                key="search.skip_relevance_filter", value="undefined"
            ),
            _make_setting(key="search.safe_search", value="{}"),
            _make_setting(key="search.search_language", value=None),
        ]

        app = _create_test_app()
        with _authenticated_client(app) as client:
            resp = self._post_fix(client, settings)
        assert resp.status_code == 200
        data = resp.get_json()
        assert "search.questions_per_iteration" in data["fixed_settings"]
        assert "search.safe_search" in data["fixed_settings"]

    def test_fixes_app_enable_notifications(self):
        settings = [_make_setting(key="app.enable_notifications", value="null")]

        app = _create_test_app()
        with _authenticated_client(app) as client:
            resp = self._post_fix(client, settings)
        assert resp.status_code == 200
        assert "app.enable_notifications" in resp.get_json()["fixed_settings"]

    def test_fixes_llm_temperature_and_max_tokens(self):
        settings = [
            _make_setting(key="llm.temperature", value=None),
            _make_setting(key="llm.max_tokens", value="undefined"),
        ]

        app = _create_test_app()
        with _authenticated_client(app) as client:
            resp = self._post_fix(client, settings)
        assert resp.status_code == 200
        data = resp.get_json()
        assert "llm.temperature" in data["fixed_settings"]
        assert "llm.max_tokens" in data["fixed_settings"]

    def test_report_unknown_key_fallback(self):
        settings = [
            _make_setting(key="report.unknown_key", value="[object Object]")
        ]

        app = _create_test_app()
        with _authenticated_client(app) as client:
            resp = self._post_fix(client, settings)
        assert resp.status_code == 200
        assert "report.unknown_key" in resp.get_json()["fixed_settings"]

    def test_empty_dict_corruption(self):
        settings = [_make_setting(key="llm.provider", value={})]

        app = _create_test_app()
        with _authenticated_client(app) as client:
            resp = self._post_fix(client, settings)
        assert resp.status_code == 200
        assert "llm.provider" in resp.get_json()["fixed_settings"]

    def test_report_searches_per_section(self):
        settings = [
            _make_setting(key="report.searches_per_section", value="null")
        ]

        app = _create_test_app()
        with _authenticated_client(app) as client:
            resp = self._post_fix(client, settings)
        assert resp.status_code == 200
        assert (
            "report.searches_per_section" in resp.get_json()["fixed_settings"]
        )


class TestSaveAllSettingsTypeCategorization:
    """save_all_settings: setting type categorization for various prefixes."""

    @patch(f"{MODULE}.set_setting", return_value=True)
    @patch(
        f"{MODULE}.coerce_setting_for_write",
        side_effect=lambda key, value, ui_element: value,
    )
    @patch(f"{MODULE}.validate_setting", return_value=(True, None))
    def test_database_prefix(self, mock_v, mock_c, mock_s):
        setting = _make_setting(
            key="database.path", value="/tmp", ui_element="text", editable=True
        )
        setting.type = "database"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"database.path": "/new"},
                )
        assert resp.status_code == 200

    @patch(f"{MODULE}.set_setting", return_value=True)
    @patch(
        f"{MODULE}.coerce_setting_for_write",
        side_effect=lambda key, value, ui_element: value,
    )
    @patch(f"{MODULE}.validate_setting", return_value=(True, None))
    def test_llm_parameters_category(self, mock_v, mock_c, mock_s):
        """llm.temperature -> category=llm_parameters."""
        setting = _make_setting(
            key="llm.temperature", value=0.7, ui_element="number", editable=True
        )
        setting.type = "llm"

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{DECORATOR_MODULE}.get_user_db_session") as mock_ctx:
                mock_session = MagicMock()
                mock_session.query.return_value.all.side_effect = [
                    [setting],
                    [setting],
                ]
                mock_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_ctx.return_value.__exit__ = MagicMock(return_value=False)
                resp = client.post(
                    f"{SETTINGS_PREFIX}/save_all_settings",
                    json={"llm.temperature": 0.9},
                )
        assert resp.status_code == 200


class TestGetBulkSettingsOuterException:
    """get_bulk_settings: outer exception -> 500."""

    def test_outer_exception(self):
        # Patch jsonify to raise on the first call (the success-path return),
        # which is outside the inner per-key try/except and will be caught by
        # the outer except block, returning a 500.  We cannot patch the module-
        # level `request` proxy directly because it requires an active request
        # context at patch-start time.
        real_jsonify = __import__("flask", fromlist=["jsonify"]).jsonify

        call_count = {"n": 0}

        def _raise_first(*args, **kwargs):
            call_count["n"] += 1
            if call_count["n"] == 1:
                raise RuntimeError("boom")
            return real_jsonify(*args, **kwargs)

        app = _create_test_app()
        with _authenticated_client(app) as client:
            with patch(f"{MODULE}.jsonify", side_effect=_raise_first):
                resp = client.get(f"{SETTINGS_PREFIX}/api/bulk")
        assert resp.status_code == 500
