"""
Targeted tests to fill remaining coverage gaps in:
- app/views/base.py (lines 36-41)
- app/views/general.py (lines 64-66, 111, 133-134, 138)
- app/views/status.py (lines 53-54, 59-60, 68-69)
- app/utils/config_validator/providers.py (lines 68-78, 86-100, 243)
- app/utils/settings_sync.py (lines 71-72, 95-98)
- app/api/logs.py (additional branches)
"""

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

import pytest

# ===========================================================================
# app/views/base.py  – kwargs context path (lines 36-41)
# ===========================================================================


@pytest.mark.unit
class TestViewsBase:
    """Tests for template_response_with_version wrapper in app/views/base.py."""

    def test_kwargs_context_version_injection(self):
        """Test version injection when context is passed as a keyword argument."""
        from app.views.base import template_response_with_version

        with patch("app.views.base.original_template_response") as mock_orig:
            mock_orig.return_value = "response"
            req = MagicMock()
            # Make the request NOT have a csrf_token on its state
            del req.state.csrf_token  # ensure AttributeError on hasattr

            result = template_response_with_version(
                "template.html",
                context={"request": req, "title": "Test"},
            )

        mock_orig.assert_called_once()
        _, kwargs = mock_orig.call_args
        assert "version" in kwargs["context"]

    def test_kwargs_context_csrf_token_injection(self):
        """Test CSRF token injection when context has request with csrf_token."""
        from app.views.base import template_response_with_version

        with patch("app.views.base.original_template_response") as mock_orig:
            mock_orig.return_value = "response"
            req = MagicMock()
            req.state.csrf_token = "test-csrf-token"

            template_response_with_version(
                "template.html",
                context={"request": req},
            )

        _, kwargs = mock_orig.call_args
        assert kwargs["context"].get("csrf_token") == "test-csrf-token"

    def test_positional_context_with_csrf_token(self):
        """Test CSRF token injection via positional args."""
        from app.views.base import template_response_with_version

        with patch("app.views.base.original_template_response") as mock_orig:
            mock_orig.return_value = "response"
            req = MagicMock()
            req.state.csrf_token = "my-csrf"

            context = {"request": req}
            template_response_with_version("template.html", context)

        args, _ = mock_orig.call_args
        assert args[1].get("csrf_token") == "my-csrf"

    def test_kwargs_context_no_request(self):
        """Test kwargs context path when request is not in context."""
        from app.views.base import template_response_with_version

        with patch("app.views.base.original_template_response") as mock_orig:
            mock_orig.return_value = "response"

            template_response_with_version(
                "template.html",
                context={"title": "No request"},
            )

        mock_orig.assert_called_once()
        _, kwargs = mock_orig.call_args
        assert "version" in kwargs["context"]

    def test_no_args_no_context(self):
        """Test with no args and no context (edge case)."""
        from app.views.base import template_response_with_version

        with patch("app.views.base.original_template_response") as mock_orig:
            mock_orig.return_value = "response"
            template_response_with_version("template.html")

        mock_orig.assert_called_once()

    def test_is_logged_in_injected_when_auth_disabled(self):
        """is_logged_in should be True when auth is disabled (single-user mode)."""
        from app.views.base import template_response_with_version

        with (
            patch("app.views.base.original_template_response") as mock_orig,
            patch("app.views.base.settings") as mock_settings,
        ):
            mock_orig.return_value = "response"
            mock_settings.auth_enabled = False
            mock_settings.multi_user_enabled = False
            mock_settings.version = "0.0.0"
            mock_settings.release_name = None
            mock_settings.ui_default_color_scheme = "system"

            req = MagicMock()
            del req.state.csrf_token

            template_response_with_version(
                "template.html",
                context={"request": req},
            )

        _, kwargs = mock_orig.call_args
        assert kwargs["context"].get("is_logged_in") is True

    def test_is_logged_in_true_when_session_user_present(self):
        """is_logged_in should be True when session contains a user."""
        from app.views.base import template_response_with_version

        with (
            patch("app.views.base.original_template_response") as mock_orig,
            patch("app.views.base.settings") as mock_settings,
        ):
            mock_orig.return_value = "response"
            mock_settings.auth_enabled = True
            mock_settings.multi_user_enabled = True
            mock_settings.version = "0.0.0"
            mock_settings.release_name = None
            mock_settings.ui_default_color_scheme = "system"

            req = MagicMock()
            req.session.get.return_value = {"email": "user@example.com"}
            del req.state.csrf_token

            template_response_with_version(
                "template.html",
                context={"request": req},
            )

        _, kwargs = mock_orig.call_args
        assert kwargs["context"].get("is_logged_in") is True

    def test_is_logged_in_false_when_no_session_user(self):
        """is_logged_in should be False when auth is enabled and no session user."""
        from app.views.base import template_response_with_version

        with (
            patch("app.views.base.original_template_response") as mock_orig,
            patch("app.views.base.settings") as mock_settings,
        ):
            mock_orig.return_value = "response"
            mock_settings.auth_enabled = True
            mock_settings.multi_user_enabled = True
            mock_settings.version = "0.0.0"
            mock_settings.release_name = None
            mock_settings.ui_default_color_scheme = "system"

            req = MagicMock()
            req.session.get.return_value = None
            del req.state.csrf_token

            template_response_with_version(
                "template.html",
                context={"request": req},
            )

        _, kwargs = mock_orig.call_args
        assert kwargs["context"].get("is_logged_in") is False

    def test_is_logged_in_false_when_request_has_no_session(self):
        """is_logged_in should default to False when request has no session attribute."""
        from app.views.base import template_response_with_version

        with (
            patch("app.views.base.original_template_response") as mock_orig,
            patch("app.views.base.settings") as mock_settings,
        ):
            mock_orig.return_value = "response"
            mock_settings.auth_enabled = True
            mock_settings.multi_user_enabled = False
            mock_settings.version = "0.0.0"
            mock_settings.release_name = None
            mock_settings.ui_default_color_scheme = "system"

            # Use a simple object without session attribute
            class NoSessionRequest:
                class state:
                    pass  # no csrf_token

            template_response_with_version(
                "template.html",
                context={"request": NoSessionRequest()},
            )

        _, kwargs = mock_orig.call_args
        assert kwargs["context"].get("is_logged_in") is False

    def test_multi_user_enabled_injected(self):
        """multi_user_enabled should be injected into every template context."""
        from app.views.base import template_response_with_version

        with (
            patch("app.views.base.original_template_response") as mock_orig,
            patch("app.views.base.settings") as mock_settings,
        ):
            mock_orig.return_value = "response"
            mock_settings.auth_enabled = False
            mock_settings.multi_user_enabled = True
            mock_settings.version = "0.0.0"
            mock_settings.release_name = None
            mock_settings.ui_default_color_scheme = "system"

            template_response_with_version(
                "template.html",
                context={"title": "no request"},
            )

        _, kwargs = mock_orig.call_args
        assert kwargs["context"].get("multi_user_enabled") is True


# ===========================================================================
# app/views/general.py  – error branches
# ===========================================================================


@pytest.mark.unit
class TestGeneralViewsAdditional:
    """Additional tests for general view error branches."""

    @patch("app.views.general.get_provider_status")
    @patch("app.views.general.validate_storage_configs")
    @patch("app.views.general.templates")
    @pytest.mark.asyncio
    async def test_db_error_logged_but_page_still_renders(self, mock_templates, mock_storage, mock_providers):
        """Test that a DB error is logged but the page still renders (lines 64-66)."""
        from app.views.general import serve_index

        mock_providers.return_value = {}
        mock_storage.return_value = {}
        mock_templates.TemplateResponse = MagicMock(return_value="response")

        with patch("app.utils.settings_service.get_setting_from_db", return_value=None):
            with patch("app.utils.setup_wizard.is_setup_required", return_value=False):
                mock_request = MagicMock()
                mock_request.query_params.get = MagicMock(return_value=None)
                mock_db = MagicMock()
                # Make DB query fail to trigger the except block (lines 64-66)
                mock_db.query.side_effect = Exception("DB failure")

                result = await serve_index(mock_request, mock_db)

        # Page should still render with 0 processed_files
        mock_templates.TemplateResponse.assert_called_once()
        call_args = mock_templates.TemplateResponse.call_args
        context = call_args[0][1]
        assert context["stats"]["processed_files"] == 0

    @patch("app.views.general.templates")
    @pytest.mark.asyncio
    async def test_favicon_not_found_raises_404(self, mock_templates):
        """Test that missing favicon raises 404 HTTPException (line 111)."""
        from fastapi import HTTPException

        from app.views.general import favicon

        with patch("pathlib.Path.exists", return_value=False):
            with pytest.raises(HTTPException) as exc_info:
                favicon()

        assert exc_info.value.status_code == 404

    @patch("app.views.general.templates")
    @pytest.mark.asyncio
    async def test_license_fallback_when_no_file_found(self, mock_templates):
        """Test that license page uses embedded text when no file found (lines 133-134, 138)."""
        from app.views.general import serve_license

        mock_templates.TemplateResponse = MagicMock(return_value="response")
        mock_request = MagicMock()

        # Patch open to raise FileNotFoundError for all paths
        with patch("builtins.open", side_effect=FileNotFoundError("No such file")):
            await serve_license(mock_request)

        # Template should be called with embedded license text
        call_args = mock_templates.TemplateResponse.call_args
        context = call_args[0][1]
        assert "Apache License" in context["license_text"]

    def test_license_page_integration_always_renders(self, client):
        """Test license page renders (with real or embedded text)."""
        response = client.get("/license")
        assert response.status_code == 200


# ===========================================================================
# app/views/status.py  – Docker exception branches
# ===========================================================================


@pytest.mark.unit
class TestStatusViewsAdditional:
    """Additional tests for status view exception branches."""

    @patch("app.views.status.get_provider_status")
    @patch("app.views.status.templates")
    @patch("app.views.status.settings")
    @patch("app.views.status.os.path.exists")
    @patch("builtins.open", new_callable=mock_open, read_data="12:docker:/abc123")
    @pytest.mark.asyncio
    async def test_git_sha_exception_in_docker_env(
        self, mock_file, mock_exists, mock_settings, mock_templates, mock_providers
    ):
        """Test Docker env: git_sha[:7] raises exception → lines 53-54."""
        from app.views.status import status_dashboard

        mock_exists.return_value = True  # Docker env exists
        mock_providers.return_value = {}
        mock_settings.version = "1.0.0"
        mock_settings.build_date = "2024-01-01"
        # A bool is not subscriptable, so git_sha[:7] raises TypeError → lines 53-54
        mock_settings.git_sha = True
        mock_settings.notification_urls = []

        mock_request = Mock()

        await status_dashboard(mock_request)

        call_args = mock_templates.TemplateResponse.call_args
        context = call_args[0][1]
        assert context["container_info"]["is_docker"] is True
        assert context["container_info"]["git_sha"] == "Unknown"

    @patch("app.views.status.get_provider_status")
    @patch("app.views.status.templates")
    @patch("app.views.status.settings")
    @patch("app.views.status.os.path.exists")
    @patch("builtins.open", new_callable=mock_open, read_data="12:docker:/abc123")
    @pytest.mark.asyncio
    async def test_runtime_info_exception_in_docker_env(
        self, mock_file, mock_exists, mock_settings, mock_templates, mock_providers
    ):
        """Test Docker env: settings.runtime_info raises exception → lines 59-60."""
        from unittest.mock import PropertyMock

        from app.views.status import status_dashboard

        mock_exists.return_value = True
        mock_providers.return_value = {}
        mock_settings.version = "1.0.0"
        mock_settings.build_date = "2024-01-01"
        mock_settings.git_sha = "abc1234567"
        mock_settings.notification_urls = []
        # Use spec to restrict runtime_info to raise AttributeError
        # since it's not a validated Settings attribute
        type(mock_settings).runtime_info = PropertyMock(side_effect=AttributeError("runtime_info not set"))

        mock_request = Mock()

        await status_dashboard(mock_request)

        call_args = mock_templates.TemplateResponse.call_args
        context = call_args[0][1]
        assert context["container_info"]["is_docker"] is True

    @patch("app.views.status.get_provider_status")
    @patch("app.views.status.templates")
    @patch("app.views.status.settings")
    @patch("app.views.status.os.path.exists")
    @pytest.mark.asyncio
    async def test_git_sha_exception_in_non_docker_env(
        self, mock_exists, mock_settings, mock_templates, mock_providers
    ):
        """Test non-Docker env: git_sha[:7] raises exception → lines 68-69."""
        from app.views.status import status_dashboard

        mock_exists.return_value = False  # Not in Docker
        mock_providers.return_value = {}
        mock_settings.version = "1.0.0"
        mock_settings.build_date = "2024-01-01"
        # A bool is not subscriptable, so git_sha[:7] raises TypeError → lines 68-69
        mock_settings.git_sha = True
        mock_settings.notification_urls = []

        mock_request = Mock()

        await status_dashboard(mock_request)

        call_args = mock_templates.TemplateResponse.call_args
        context = call_args[0][1]
        assert context["container_info"]["is_docker"] is False
        assert context["container_info"]["git_sha"] == "Unknown"


# ===========================================================================
# app/utils/config_validator/providers.py – alternative AI providers
# ===========================================================================


@pytest.mark.unit
class TestProvidersAlternativeAI:
    """Tests for alternative AI provider branches in get_provider_status."""

    def test_anthropic_configured(self):
        """Test provider status with anthropic AI provider (lines 68-69, 86-87)."""
        from app.utils.config_validator.providers import get_provider_status

        with patch("app.utils.config_validator.providers.settings") as mock_settings:
            mock_settings.ai_provider = "anthropic"
            mock_settings.ai_model = "claude-3-5-sonnet"
            mock_settings.anthropic_api_key = "sk-ant-key"
            mock_settings.auth_enabled = False
            mock_settings.notification_urls = []
            mock_settings.azure_ai_key = None
            mock_settings.azure_endpoint = None
            mock_settings.azure_region = "eastus"
            _set_minimal_provider_settings(mock_settings)

            result = get_provider_status()

        ai_provider = result["AI Provider"]
        assert ai_provider["configured"] is True
        assert ai_provider["details"]["provider"] == "anthropic"
        assert "api_key" in ai_provider["details"]

    def test_gemini_configured(self):
        """Test provider status with gemini AI provider (lines 70-71, 88-89)."""
        from app.utils.config_validator.providers import get_provider_status

        with patch("app.utils.config_validator.providers.settings") as mock_settings:
            mock_settings.ai_provider = "gemini"
            mock_settings.ai_model = "gemini-pro"
            mock_settings.gemini_api_key = "gemini-key-123"
            mock_settings.auth_enabled = False
            mock_settings.notification_urls = []
            mock_settings.azure_ai_key = None
            mock_settings.azure_endpoint = None
            mock_settings.azure_region = "eastus"
            _set_minimal_provider_settings(mock_settings)

            result = get_provider_status()

        ai_provider = result["AI Provider"]
        assert ai_provider["configured"] is True
        assert ai_provider["details"]["provider"] == "gemini"
        assert "api_key" in ai_provider["details"]

    def test_ollama_configured(self):
        """Test provider status with ollama AI provider (lines 72-73, 90-91)."""
        from app.utils.config_validator.providers import get_provider_status

        with patch("app.utils.config_validator.providers.settings") as mock_settings:
            mock_settings.ai_provider = "ollama"
            mock_settings.ai_model = "llama2"
            mock_settings.ollama_base_url = "http://localhost:11434"
            mock_settings.auth_enabled = False
            mock_settings.notification_urls = []
            mock_settings.azure_ai_key = None
            mock_settings.azure_endpoint = None
            mock_settings.azure_region = "eastus"
            _set_minimal_provider_settings(mock_settings)

            result = get_provider_status()

        ai_provider = result["AI Provider"]
        assert ai_provider["configured"] is True
        assert ai_provider["details"]["provider"] == "ollama"
        assert "base_url" in ai_provider["details"]

    def test_openrouter_configured(self):
        """Test provider status with openrouter AI provider (lines 74-75, 92-94)."""
        from app.utils.config_validator.providers import get_provider_status

        with patch("app.utils.config_validator.providers.settings") as mock_settings:
            mock_settings.ai_provider = "openrouter"
            mock_settings.ai_model = "openai/gpt-4"
            mock_settings.openrouter_api_key = "or-key-123"
            mock_settings.openrouter_base_url = "https://openrouter.ai/api/v1"
            mock_settings.auth_enabled = False
            mock_settings.notification_urls = []
            mock_settings.azure_ai_key = None
            mock_settings.azure_endpoint = None
            mock_settings.azure_region = "eastus"
            _set_minimal_provider_settings(mock_settings)

            result = get_provider_status()

        ai_provider = result["AI Provider"]
        assert ai_provider["configured"] is True
        assert ai_provider["details"]["provider"] == "openrouter"
        assert "api_key" in ai_provider["details"]
        assert "base_url" in ai_provider["details"]

    def test_portkey_configured_with_virtual_key(self):
        """Test provider status with portkey AI provider including virtual key (lines 76-77, 95-100)."""
        from app.utils.config_validator.providers import get_provider_status

        with patch("app.utils.config_validator.providers.settings") as mock_settings:
            mock_settings.ai_provider = "portkey"
            mock_settings.ai_model = "claude-3"
            mock_settings.portkey_api_key = "pk-key-123"
            mock_settings.portkey_virtual_key = "vk-123"
            mock_settings.portkey_config = None
            mock_settings.auth_enabled = False
            mock_settings.notification_urls = []
            mock_settings.azure_ai_key = None
            mock_settings.azure_endpoint = None
            mock_settings.azure_region = "eastus"
            _set_minimal_provider_settings(mock_settings)

            result = get_provider_status()

        ai_provider = result["AI Provider"]
        assert ai_provider["configured"] is True
        assert ai_provider["details"]["provider"] == "portkey"
        assert "api_key" in ai_provider["details"]
        assert "virtual_key" in ai_provider["details"]

    def test_portkey_configured_with_config(self):
        """Test provider status with portkey AI provider including config."""
        from app.utils.config_validator.providers import get_provider_status

        with patch("app.utils.config_validator.providers.settings") as mock_settings:
            mock_settings.ai_provider = "portkey"
            mock_settings.ai_model = "claude-3"
            mock_settings.portkey_api_key = "pk-key-123"
            mock_settings.portkey_virtual_key = None
            mock_settings.portkey_config = '{"strategy": "loadbalance"}'
            mock_settings.auth_enabled = False
            mock_settings.notification_urls = []
            mock_settings.azure_ai_key = None
            mock_settings.azure_endpoint = None
            mock_settings.azure_region = "eastus"
            _set_minimal_provider_settings(mock_settings)

            result = get_provider_status()

        ai_provider = result["AI Provider"]
        assert "config" in ai_provider["details"]

    def test_unknown_ai_provider_not_configured(self):
        """Test that unknown provider returns configured=False (line 78 – return False)."""
        from app.utils.config_validator.providers import get_provider_status

        with patch("app.utils.config_validator.providers.settings") as mock_settings:
            mock_settings.ai_provider = "unknown_provider"
            mock_settings.ai_model = None
            mock_settings.auth_enabled = False
            mock_settings.notification_urls = []
            mock_settings.azure_ai_key = None
            mock_settings.azure_endpoint = None
            mock_settings.azure_region = "eastus"
            _set_minimal_provider_settings(mock_settings)

            result = get_provider_status()

        ai_provider = result["AI Provider"]
        assert ai_provider["configured"] is False

    def test_nextcloud_url_with_remote_php(self):
        """Test that nextcloud base URL is extracted correctly (line 243)."""
        from app.utils.config_validator.providers import get_provider_status

        with patch("app.utils.config_validator.providers.settings") as mock_settings:
            mock_settings.ai_provider = "openai"
            mock_settings.ai_model = "gpt-4o-mini"
            mock_settings.openai_api_key = "sk-key"
            mock_settings.openai_base_url = "https://api.openai.com/v1"
            mock_settings.auth_enabled = False
            mock_settings.notification_urls = []
            mock_settings.azure_ai_key = None
            mock_settings.azure_endpoint = None
            mock_settings.azure_region = "eastus"
            # NextCloud URL with /remote.php should be split
            mock_settings.nextcloud_upload_url = "https://cloud.example.com/remote.php/dav/files/user"
            mock_settings.nextcloud_username = "user"
            mock_settings.nextcloud_password = "pass"
            mock_settings.nextcloud_folder = "/Docs"
            _set_minimal_provider_settings(mock_settings)

            result = get_provider_status()

        nextcloud = result["NextCloud"]
        assert nextcloud["details"]["base_url"] == "https://cloud.example.com"


def _set_minimal_provider_settings(mock_settings):
    """Helper to set the minimal settings attributes needed by get_provider_status."""
    defaults = {
        "authentik_client_id": None,
        "authentik_client_secret": None,
        "authentik_config_url": None,
        "admin_username": None,
        "oauth_provider_name": None,
        "session_secret": "secret",
        "notify_on_task_failure": True,
        "notify_on_credential_failure": True,
        "notify_on_startup": True,
        "notify_on_shutdown": False,
        "dropbox_app_key": None,
        "dropbox_app_secret": None,
        "dropbox_refresh_token": None,
        "dropbox_folder": "/",
        "email_host": None,
        "email_default_recipient": None,
        "email_port": 587,
        "email_username": None,
        "email_password": None,
        "email_use_tls": True,
        "email_sender": None,
        "dest_email_host": None,
        "dest_email_default_recipient": None,
        "dest_email_port": 587,
        "dest_email_username": None,
        "dest_email_password": None,
        "dest_email_use_tls": True,
        "dest_email_sender": None,
        "ftp_host": None,
        "ftp_username": None,
        "ftp_password": None,
        "ftp_port": 21,
        "ftp_folder": "/",
        "ftp_use_tls": True,
        "ftp_allow_plaintext": False,
        "google_drive_client_id": None,
        "google_drive_client_secret": None,
        "google_drive_refresh_token": None,
        "google_drive_credentials_json": None,
        "google_drive_use_oauth": False,
        "google_drive_folder_id": None,
        "google_drive_delegate_to": None,
        "nextcloud_upload_url": None,
        "nextcloud_username": None,
        "nextcloud_password": None,
        "nextcloud_folder": "/",
        "onedrive_client_id": None,
        "onedrive_client_secret": None,
        "onedrive_refresh_token": None,
        "onedrive_tenant_id": None,
        "onedrive_folder_path": "/",
        "paperless_url": None,
        "paperless_api_token": None,
        "paperless_correspondent_name": None,
        "paperless_document_type_name": None,
        "s3_bucket_name": None,
        "s3_region": None,
        "aws_access_key_id": None,
        "aws_secret_access_key": None,
        "s3_folder_prefix": None,
        "sftp_host": None,
        "sftp_username": None,
        "sftp_password": None,
        "sftp_port": 22,
        "sftp_folder": "/",
        "sftp_private_key": None,
        "webdav_url": None,
        "webdav_username": None,
        "webdav_password": None,
        "webdav_folder": "/",
        "imap_host": None,
        "imap_username": None,
        "imap_password": None,
        "imap_folder": "INBOX",
        "gotenberg_url": "http://localhost:3000",
        "rclone_remote": None,
        "rclone_base_path": None,
        "uptime_kuma_push_url": None,
        # AI provider specifics (set None to avoid AttributeError)
        "anthropic_api_key": None,
        "gemini_api_key": None,
        "ollama_base_url": None,
        "openrouter_api_key": None,
        "openrouter_base_url": None,
        "portkey_api_key": None,
        "portkey_virtual_key": None,
        "portkey_config": None,
        "litellm": None,
        "openai_api_key": None,
        "openai_base_url": "https://api.openai.com/v1",
    }
    for attr, val in defaults.items():
        if not hasattr(mock_settings, attr) or getattr(mock_settings, attr, "NOTSET") == "NOTSET":
            setattr(mock_settings, attr, val)


# ===========================================================================
# app/utils/settings_sync.py  – reload failure and signal handler
# ===========================================================================


@pytest.mark.unit
class TestSettingsSyncAdditional:
    """Additional tests for settings_sync covering reload failure branch."""

    def test_notify_settings_updated_redis_failure_logs_warning(self):
        """Test that a Redis failure is logged, not raised (lines 58-59)."""
        from app.utils.settings_sync import notify_settings_updated

        with patch("app.utils.settings_sync.redis") as mock_redis_module:
            mock_redis_module.from_url.side_effect = Exception("Redis connection failed")
            with patch("app.utils.settings_sync.logger") as mock_logger:
                notify_settings_updated()
                mock_logger.warning.assert_any_call(
                    "Could not publish settings update to Redis: Redis connection failed"
                )

    def test_reload_failure_is_logged_not_raised(self):
        """Test that a reload failure is logged, not raised (lines 71-72)."""
        from app.utils.settings_sync import notify_settings_updated

        with patch("app.utils.settings_sync.redis") as mock_redis_module:
            mock_redis_module.from_url.return_value = MagicMock()  # Redis OK
            with patch("app.utils.config_loader.reload_settings_from_db", side_effect=Exception("reload failed")):
                with patch("app.utils.settings_sync.logger") as mock_logger:
                    # Should not raise despite reload failure
                    notify_settings_updated()
                    mock_logger.warning.assert_any_call("Could not reload in-process settings: reload failed")

    def test_notify_settings_updated_ocr_failure_logs_warning(self):
        """Test that OCR check failure is logged, not raised (lines 82-83)."""
        from app.utils.settings_sync import notify_settings_updated

        with patch("app.utils.settings_sync.redis") as mock_redis_module:
            mock_redis_module.from_url.return_value = MagicMock()  # Redis OK
            with patch("app.utils.config_loader.reload_settings_from_db"):
                with patch(
                    "app.utils.ocr_language_manager.ensure_ocr_languages_async", side_effect=Exception("OCR failed")
                ):
                    with patch("app.utils.settings_sync.logger") as mock_logger:
                        notify_settings_updated()
                        mock_logger.warning.assert_any_call("Could not schedule OCR language check: OCR failed")

    def test_signal_handler_ocr_check_failure_logs_warning(self):
        """Test that signal handler logs warning if OCR language check fails on worker (lines 115-116)."""
        from app.utils.settings_sync import register_settings_reload_signal

        handler_fn = None

        def capture_connect(fn=None, weak=None, **kwargs):
            nonlocal handler_fn
            if fn is not None:
                handler_fn = fn
                return fn

            def decorator(func):
                nonlocal handler_fn
                handler_fn = func
                return func

            return decorator

        with patch("app.utils.settings_sync.task_prerun") as mock_signal:
            mock_signal.connect = capture_connect
            register_settings_reload_signal()

        assert handler_fn is not None

        mock_redis = MagicMock()
        mock_redis.get.return_value = b"1234567890.0"

        with patch("app.utils.settings_sync.redis") as mock_redis_mod:
            mock_redis_mod.from_url.return_value = mock_redis
            with patch("app.utils.config_loader.reload_settings_from_db"):
                with patch("app.utils.settings_sync._last_seen_version", ""):
                    with patch(
                        "app.utils.ocr_language_manager.ensure_ocr_languages_async",
                        side_effect=Exception("Worker OCR fail"),
                    ):
                        with patch("app.utils.settings_sync.logger") as mock_logger:
                            handler_fn(sender=None)
                            mock_logger.warning.assert_any_call(
                                "Could not schedule OCR language check on worker: Worker OCR fail"
                            )

    def test_signal_handler_reloads_on_version_change(self):
        """Test the task_prerun signal handler reloads settings when version changes (lines 95-98)."""
        from app.utils.settings_sync import register_settings_reload_signal

        handler_fn = None

        # Capture the handler; the decorator pattern means connect(weak=False)
        # returns a decorator, which is then applied to _reload_if_stale
        def capture_connect(fn=None, weak=None, **kwargs):
            nonlocal handler_fn
            if fn is not None:
                handler_fn = fn
                return fn

            def decorator(func):
                nonlocal handler_fn
                handler_fn = func
                return func

            return decorator

        with patch("app.utils.settings_sync.task_prerun") as mock_signal:
            mock_signal.connect = capture_connect
            register_settings_reload_signal()

        assert handler_fn is not None

        # Simulate handler being called with a new version
        mock_redis = MagicMock()
        mock_redis.get.return_value = b"1234567890.0"  # new version

        with patch("app.utils.settings_sync.redis") as mock_redis_mod:
            mock_redis_mod.from_url.return_value = mock_redis
            with patch("app.utils.config_loader.reload_settings_from_db") as mock_reload:
                with patch("app.utils.settings_sync._last_seen_version", ""):
                    handler_fn(sender=None)  # call the signal handler
                    mock_reload.assert_called_once()

    def test_signal_handler_skips_reload_when_version_unchanged(self):
        """Test that handler skips reload when version is same as last seen."""
        import app.utils.settings_sync as sync_module
        from app.utils.settings_sync import register_settings_reload_signal

        handler_fn = None

        def capture_connect(fn=None, weak=None, **kwargs):
            nonlocal handler_fn
            if fn is not None:
                handler_fn = fn
                return fn

            def decorator(func):
                nonlocal handler_fn
                handler_fn = func
                return func

            return decorator

        with patch("app.utils.settings_sync.task_prerun") as mock_signal:
            mock_signal.connect = capture_connect
            register_settings_reload_signal()

        assert handler_fn is not None

        version = "999.0"
        mock_redis = MagicMock()
        mock_redis.get.return_value = version.encode()

        with patch("app.utils.settings_sync.redis") as mock_redis_mod:
            mock_redis_mod.from_url.return_value = mock_redis
            with patch("app.utils.config_loader.reload_settings_from_db") as mock_reload:
                # Set last seen to same version
                sync_module._last_seen_version = version
                handler_fn(sender=None)
                mock_reload.assert_not_called()

    def test_signal_handler_handles_redis_failure(self):
        """Test that signal handler skips gracefully on Redis failure."""
        from app.utils.settings_sync import register_settings_reload_signal

        handler_fn = None

        def capture_connect(fn=None, weak=None, **kwargs):
            nonlocal handler_fn
            if fn is not None:
                handler_fn = fn
                return fn

            def decorator(func):
                nonlocal handler_fn
                handler_fn = func
                return func

            return decorator

        with patch("app.utils.settings_sync.task_prerun") as mock_signal:
            mock_signal.connect = capture_connect
            register_settings_reload_signal()

        assert handler_fn is not None

        with patch("app.utils.settings_sync.redis") as mock_redis_mod:
            mock_redis_mod.from_url.side_effect = Exception("Redis down")
            # Should not raise
            handler_fn(sender=None)

    def test_signal_handler_no_version_returned(self):
        """Test that handler does nothing if Redis returns None for version."""
        from app.utils.settings_sync import register_settings_reload_signal

        handler_fn = None

        def capture_connect(fn=None, weak=None, **kwargs):
            nonlocal handler_fn
            if fn is not None:
                handler_fn = fn
                return fn

            def decorator(func):
                nonlocal handler_fn
                handler_fn = func
                return func

            return decorator

        with patch("app.utils.settings_sync.task_prerun") as mock_signal:
            mock_signal.connect = capture_connect
            register_settings_reload_signal()

        assert handler_fn is not None

        mock_redis = MagicMock()
        mock_redis.get.return_value = None  # Return None for version

        with patch("app.utils.settings_sync.redis") as mock_redis_mod:
            mock_redis_mod.from_url.return_value = mock_redis
            with patch("app.utils.config_loader.reload_settings_from_db") as mock_reload:
                handler_fn(sender=None)
                mock_reload.assert_not_called()

    def test_signal_handler_ocr_language_manager_exception(self):
        """Test that OCR language check exception inside handler is caught and logged."""
        from app.utils.settings_sync import register_settings_reload_signal

        handler_fn = None

        def capture_connect(fn=None, weak=None, **kwargs):
            nonlocal handler_fn
            if fn is not None:
                handler_fn = fn
                return fn

            def decorator(func):
                nonlocal handler_fn
                handler_fn = func
                return func

            return decorator

        with patch("app.utils.settings_sync.task_prerun") as mock_signal:
            mock_signal.connect = capture_connect
            register_settings_reload_signal()

        assert handler_fn is not None

        mock_redis = MagicMock()
        mock_redis.get.return_value = b"9999999.0"  # New version

        with patch("app.utils.settings_sync.redis") as mock_redis_mod:
            mock_redis_mod.from_url.return_value = mock_redis
            with patch("app.utils.config_loader.reload_settings_from_db") as mock_reload:
                with patch("app.utils.settings_sync._last_seen_version", "111.0"):
                    with patch("app.utils.settings_sync.logger") as mock_logger:
                        with patch(
                            "app.utils.ocr_language_manager.ensure_ocr_languages_async",
                            side_effect=Exception("OCR failed"),
                        ):
                            handler_fn(sender=None)
                            mock_reload.assert_called_once()
                            mock_logger.warning.assert_called_with(
                                "Could not schedule OCR language check on worker: OCR failed"
                            )


# ===========================================================================
# app/api/logs.py – additional branches
# ===========================================================================


@pytest.mark.integration
class TestLogsApiAdditional:
    """Additional tests for logs API to improve branch coverage."""

    def test_list_logs_filter_by_file_id(self, client, db_session):
        """Test filtering logs by file_id."""
        from app.models import FileRecord, ProcessingLog

        file_record = FileRecord(
            filehash="hash123",
            original_filename="test.pdf",
            local_filename="/tmp/test.pdf",
            file_size=1024,
            mime_type="application/pdf",
        )
        db_session.add(file_record)
        db_session.commit()

        log = ProcessingLog(
            file_id=file_record.id,
            task_id="aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
            step_name="process",
            status="success",
            message="Done",
        )
        db_session.add(log)
        db_session.commit()

        response = client.get(f"/api/logs?file_id={file_record.id}")
        assert response.status_code == 200
        data = response.json()
        assert all(entry["file_id"] == file_record.id for entry in data)

    def test_list_logs_with_null_timestamp(self, client, db_session):
        """Test log with null timestamp serializes to None."""
        from app.models import ProcessingLog

        log = ProcessingLog(
            task_id="bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb",
            step_name="step",
            status="success",
            message="msg",
        )
        log.timestamp = None
        db_session.add(log)
        db_session.commit()

        response = client.get("/api/logs")
        assert response.status_code == 200

    def test_get_file_logs_returns_200(self, client, db_session):
        """Test get file logs endpoint returns data."""
        from app.models import FileRecord, ProcessingLog

        file_record = FileRecord(
            filehash="hash456",
            original_filename="test2.pdf",
            local_filename="/tmp/test2.pdf",
            file_size=512,
            mime_type="application/pdf",
        )
        db_session.add(file_record)
        db_session.commit()

        log = ProcessingLog(
            file_id=file_record.id,
            task_id="cccccccc-cccc-4ccc-8ccc-cccccccccccc",
            step_name="step",
            status="success",
            message="msg",
        )
        db_session.add(log)
        db_session.commit()

        response = client.get(f"/api/logs/file/{file_record.id}")
        assert response.status_code == 200
        data = response.json()
        assert len(data["logs"]) >= 1
