"""
Comprehensive tests for research_library/routes/rag_routes.py

Tests cover:
- get_rag_service function
- RAG service initialization
- Executor management
"""

import pytest
from unittest.mock import Mock, patch, MagicMock
from local_deep_research.constants import (
    DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS_JSON,
)
from local_deep_research.web.auth import auth_bp


class TestGetAutoIndexExecutor:
    """Tests for _get_auto_index_executor function."""

    def test_executor_creation(self):
        """Test that executor is created lazily."""
        from local_deep_research.research_library.routes import rag_routes

        # Reset global state
        rag_routes._auto_index_executor = None

        try:
            executor = rag_routes._get_auto_index_executor()

            assert executor is not None
            assert rag_routes._auto_index_executor is not None
        finally:
            rag_routes._shutdown_auto_index_executor()

    def test_executor_reused(self):
        """Test that executor is reused on subsequent calls."""
        from local_deep_research.research_library.routes import rag_routes

        # Reset global state
        rag_routes._auto_index_executor = None

        try:
            executor1 = rag_routes._get_auto_index_executor()
            executor2 = rag_routes._get_auto_index_executor()

            assert executor1 is executor2
        finally:
            rag_routes._shutdown_auto_index_executor()


class TestShutdownAutoIndexExecutor:
    """Tests for _shutdown_auto_index_executor function."""

    def test_shutdown_clears_executor(self):
        """Test that shutdown clears the executor."""
        from local_deep_research.research_library.routes import rag_routes

        # Create executor first
        _ = rag_routes._get_auto_index_executor()
        assert rag_routes._auto_index_executor is not None

        # Shutdown
        rag_routes._shutdown_auto_index_executor()

        assert rag_routes._auto_index_executor is None

    def test_shutdown_handles_none(self):
        """Test that shutdown handles None executor."""
        from local_deep_research.research_library.routes import rag_routes

        rag_routes._auto_index_executor = None

        # Should not raise
        rag_routes._shutdown_auto_index_executor()


class TestGetRagService:
    """Tests for get_rag_service function."""

    @pytest.fixture
    def mock_settings(self):
        """Create mock settings manager."""
        mock = Mock()
        mock.get_setting.side_effect = lambda key, default=None: {
            # Explicit permissive scope: these tests exercise the settings
            # plumbing with FAKE provider names; the registered default
            # (adaptive) would resolve PRIVATE_ONLY for the library and
            # deny them as provider_unknown.
            "policy.egress_scope": "both",
            "local_search_embedding_model": "test-model",
            "local_search_embedding_provider": "sentence_transformers",
            "local_search_chunk_size": "1000",
            "local_search_chunk_overlap": "200",
            "local_search_splitter_type": "recursive",
            "local_search_text_separators": DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS_JSON,
            "local_search_distance_metric": "cosine",
            "local_search_normalize_vectors": True,
            "local_search_index_type": "flat",
        }.get(key, default)
        return mock

    def test_get_rag_service_no_collection(self, mock_settings):
        """Test getting RAG service without collection."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from local_deep_research.research_library.routes.rag_routes import (
            get_rag_service,
        )

        with patch(
            "local_deep_research.research_library.services.rag_service_factory.get_settings_manager",
            return_value=mock_settings,
        ):
            with patch(
                "local_deep_research.research_library.routes.rag_routes.session",
                {"username": "testuser"},
            ):
                with patch(
                    "local_deep_research.research_library.services.rag_service_factory.get_user_db_session"
                ) as mock_ctx:
                    mock_ctx.return_value.__enter__ = Mock(
                        return_value=MagicMock()
                    )
                    mock_ctx.return_value.__exit__ = Mock(return_value=False)

                    with patch(
                        "local_deep_research.research_library.services.rag_service_factory.LibraryRAGService"
                    ) as mock_rag:
                        mock_service = Mock()
                        mock_rag.return_value = mock_service

                        service = get_rag_service()

                        assert service == mock_service
                        mock_rag.assert_called_once()

    def test_get_rag_service_with_collection_existing_settings(
        self, mock_settings
    ):
        """Test getting RAG service with collection that has existing settings."""
        from local_deep_research.research_library.routes.rag_routes import (
            get_rag_service,
        )

        mock_collection = Mock()
        mock_collection.embedding_model = "existing-model"
        mock_collection.embedding_model_type = Mock()
        mock_collection.embedding_model_type.value = "existing_provider"
        mock_collection.chunk_size = 500
        mock_collection.chunk_overlap = 100
        mock_collection.splitter_type = "simple"
        mock_collection.text_separators = ["\n"]
        mock_collection.distance_metric = "euclidean"
        mock_collection.normalize_vectors = False
        mock_collection.index_type = "hnsw"

        mock_db_session = MagicMock()
        mock_query = MagicMock()
        mock_db_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.first.return_value = mock_collection

        with patch(
            "local_deep_research.research_library.services.rag_service_factory.get_settings_manager",
            return_value=mock_settings,
        ):
            with patch(
                "local_deep_research.research_library.routes.rag_routes.session",
                {"username": "testuser"},
            ):
                with patch(
                    "local_deep_research.research_library.services.rag_service_factory.get_user_db_session"
                ) as mock_ctx:
                    mock_ctx.return_value.__enter__ = Mock(
                        return_value=mock_db_session
                    )
                    mock_ctx.return_value.__exit__ = Mock(return_value=False)

                    with patch(
                        "local_deep_research.research_library.services.rag_service_factory.LibraryRAGService"
                    ) as mock_rag:
                        mock_service = Mock()
                        mock_rag.return_value = mock_service

                        service = get_rag_service(collection_id="col123")

                        assert service == mock_service
                        # Should use collection's settings
                        call_kwargs = mock_rag.call_args[1]
                        assert (
                            call_kwargs["embedding_model"] == "existing-model"
                        )

    def test_get_rag_service_with_new_collection(self, mock_settings):
        """Test getting RAG service with new collection (no stored settings)."""
        from local_deep_research.research_library.routes.rag_routes import (
            get_rag_service,
        )

        mock_collection = Mock()
        mock_collection.embedding_model = None  # No existing settings

        mock_db_session = MagicMock()
        mock_query = MagicMock()
        mock_db_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.first.return_value = mock_collection

        with patch(
            "local_deep_research.research_library.services.rag_service_factory.get_settings_manager",
            return_value=mock_settings,
        ):
            with patch(
                "local_deep_research.research_library.routes.rag_routes.session",
                {"username": "testuser"},
            ):
                with patch(
                    "local_deep_research.research_library.services.rag_service_factory.get_user_db_session"
                ) as mock_ctx:
                    mock_ctx.return_value.__enter__ = Mock(
                        return_value=mock_db_session
                    )
                    mock_ctx.return_value.__exit__ = Mock(return_value=False)

                    with patch(
                        "local_deep_research.research_library.services.rag_service_factory.LibraryRAGService"
                    ) as mock_rag:
                        mock_service = Mock()
                        mock_rag.return_value = mock_service

                        service = get_rag_service(collection_id="new_col")

                        assert service == mock_service
                        # Should use default settings
                        call_kwargs = mock_rag.call_args[1]
                        assert call_kwargs["embedding_model"] == "test-model"

    def test_use_defaults_skips_stored_settings(self, mock_settings):
        """Test that use_defaults=True ignores stored collection settings.

        When force-reindexing, the user wants the current default model,
        not the model stored in the collection from a previous index.
        """
        from local_deep_research.research_library.routes.rag_routes import (
            get_rag_service,
        )

        mock_collection = Mock()
        mock_collection.embedding_model = "old-stored-model"
        mock_collection.embedding_model_type = Mock()
        mock_collection.embedding_model_type.value = "old_provider"
        mock_collection.chunk_size = 500
        mock_collection.chunk_overlap = 100

        mock_db_session = MagicMock()
        mock_query = MagicMock()
        mock_db_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.first.return_value = mock_collection

        with patch(
            "local_deep_research.research_library.services.rag_service_factory.get_settings_manager",
            return_value=mock_settings,
        ):
            with patch(
                "local_deep_research.research_library.routes.rag_routes.session",
                {"username": "testuser"},
            ):
                with patch(
                    "local_deep_research.research_library.services.rag_service_factory.get_user_db_session"
                ) as mock_ctx:
                    mock_ctx.return_value.__enter__ = Mock(
                        return_value=mock_db_session
                    )
                    mock_ctx.return_value.__exit__ = Mock(return_value=False)

                    with patch(
                        "local_deep_research.research_library.services.rag_service_factory.LibraryRAGService"
                    ) as mock_rag:
                        mock_service = Mock()
                        mock_rag.return_value = mock_service

                        service = get_rag_service(
                            collection_id="col123", use_defaults=True
                        )

                        assert service == mock_service
                        # Should use DEFAULT settings, not stored "old-stored-model"
                        call_kwargs = mock_rag.call_args[1]
                        assert call_kwargs["embedding_model"] == "test-model"

    def test_use_defaults_false_uses_stored_settings(self, mock_settings):
        """Test that use_defaults=False (default) uses stored collection settings."""
        from local_deep_research.research_library.routes.rag_routes import (
            get_rag_service,
        )

        mock_collection = Mock()
        mock_collection.embedding_model = "stored-model"
        mock_collection.embedding_model_type = Mock()
        mock_collection.embedding_model_type.value = "stored_provider"
        mock_collection.chunk_size = 500
        mock_collection.chunk_overlap = 100
        mock_collection.splitter_type = "simple"
        mock_collection.text_separators = ["\n"]
        mock_collection.distance_metric = "euclidean"
        mock_collection.normalize_vectors = False
        mock_collection.index_type = "hnsw"

        mock_db_session = MagicMock()
        mock_query = MagicMock()
        mock_db_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.first.return_value = mock_collection

        with patch(
            "local_deep_research.research_library.services.rag_service_factory.get_settings_manager",
            return_value=mock_settings,
        ):
            with patch(
                "local_deep_research.research_library.routes.rag_routes.session",
                {"username": "testuser"},
            ):
                with patch(
                    "local_deep_research.research_library.services.rag_service_factory.get_user_db_session"
                ) as mock_ctx:
                    mock_ctx.return_value.__enter__ = Mock(
                        return_value=mock_db_session
                    )
                    mock_ctx.return_value.__exit__ = Mock(return_value=False)

                    with patch(
                        "local_deep_research.research_library.services.rag_service_factory.LibraryRAGService"
                    ) as mock_rag:
                        mock_service = Mock()
                        mock_rag.return_value = mock_service

                        service = get_rag_service(
                            collection_id="col123", use_defaults=False
                        )

                        assert service == mock_service
                        # Should use STORED settings
                        call_kwargs = mock_rag.call_args[1]
                        assert call_kwargs["embedding_model"] == "stored-model"

    def test_json_text_separators_parsing(self, mock_settings):
        """Test that JSON text separators are properly parsed."""
        from local_deep_research.research_library.routes.rag_routes import (
            get_rag_service,
        )

        with patch(
            "local_deep_research.research_library.services.rag_service_factory.get_settings_manager",
            return_value=mock_settings,
        ):
            with patch(
                "local_deep_research.research_library.routes.rag_routes.session",
                {"username": "testuser"},
            ):
                with patch(
                    "local_deep_research.research_library.services.rag_service_factory.get_user_db_session"
                ) as mock_ctx:
                    mock_ctx.return_value.__enter__ = Mock(
                        return_value=MagicMock()
                    )
                    mock_ctx.return_value.__exit__ = Mock(return_value=False)

                    with patch(
                        "local_deep_research.research_library.services.rag_service_factory.LibraryRAGService"
                    ) as mock_rag:
                        mock_service = Mock()
                        mock_rag.return_value = mock_service

                        get_rag_service()

                        # Check that text_separators was parsed
                        call_kwargs = mock_rag.call_args[1]
                        assert isinstance(call_kwargs["text_separators"], list)

    def test_invalid_json_text_separators_falls_back(self):
        """Invalid JSON text separators should log warning and use defaults."""
        from local_deep_research.research_library.routes.rag_routes import (
            get_rag_service,
        )

        mock_settings = Mock()
        mock_settings.get_setting.side_effect = lambda key, default=None: {
            "local_search_embedding_model": "test-model",
            "local_search_embedding_provider": "sentence_transformers",
            "local_search_chunk_size": "1000",
            "local_search_chunk_overlap": "200",
            "local_search_splitter_type": "recursive",
            "local_search_text_separators": "invalid json",  # Invalid!
            "local_search_distance_metric": "cosine",
            "local_search_normalize_vectors": True,
            "local_search_index_type": "flat",
        }.get(key, default)

        with (
            patch(
                "local_deep_research.research_library.services.rag_service_factory.get_settings_manager",
                return_value=mock_settings,
            ),
            patch(
                "local_deep_research.research_library.services.rag_service_factory.LibraryRAGService",
            ) as mock_rag,
        ):
            with patch(
                "local_deep_research.research_library.routes.rag_routes.session",
                {"username": "testuser"},
            ):
                with patch(
                    "local_deep_research.research_library.services.rag_service_factory.get_user_db_session"
                ) as mock_ctx:
                    mock_ctx.return_value.__enter__ = Mock(
                        return_value=MagicMock()
                    )
                    mock_ctx.return_value.__exit__ = Mock(return_value=False)
                    get_rag_service()
                    # Should use default separators after warning
                    call_kwargs = mock_rag.call_args[1]
                    assert call_kwargs["text_separators"] == [
                        "\n\n",
                        "\n",
                        ". ",
                        " ",
                        "",
                    ]


class TestGetTextSeparatorsHelper:
    """Tests for _get_text_separators helper in rag_routes."""

    def test_parses_json_string(self):
        from local_deep_research.research_library.routes.rag_routes import (
            _get_text_separators,
        )

        mock_settings = Mock()
        mock_settings.get_setting.return_value = '["\\n", ". "]'

        separators = _get_text_separators(mock_settings)

        assert separators == ["\n", ". "]

    def test_invalid_string_falls_back_to_defaults(self):
        """A value that is not valid JSON (e.g. a not-yet-migrated corrupt
        row) falls back to the default separators rather than being kept raw
        or ast-recovered. Migration #4298 heals existing corrupt rows."""
        from local_deep_research.constants import (
            DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS,
        )
        from local_deep_research.research_library.routes.rag_routes import (
            _get_text_separators,
        )

        mock_settings = Mock()
        mock_settings.get_setting.return_value = "invalid json"

        separators = _get_text_separators(mock_settings)

        assert separators == DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS

    def test_python_repr_corrupt_value_falls_back_to_defaults(self):
        """A Python-repr (single-quoted) corrupt value is not valid JSON; it
        is no longer ast-recovered and falls back to the defaults."""
        from local_deep_research.constants import (
            DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS,
        )
        from local_deep_research.research_library.routes.rag_routes import (
            _get_text_separators,
        )

        mock_settings = Mock()
        mock_settings.get_setting.return_value = "['\\n\\n', '\\n']"

        separators = _get_text_separators(mock_settings)

        assert separators == DEFAULT_LOCAL_SEARCH_TEXT_SEPARATORS

    def test_passes_through_list_values(self):
        from local_deep_research.research_library.routes.rag_routes import (
            _get_text_separators,
        )

        mock_settings = Mock()
        mock_settings.get_setting.return_value = ["\n"]

        separators = _get_text_separators(mock_settings)

        assert separators == ["\n"]


class TestRagBlueprintImport:
    """Tests for RAG blueprint import."""

    def test_blueprint_exists(self):
        """Test that RAG blueprint exists."""
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        assert rag_bp is not None
        assert rag_bp.name == "rag"
        assert rag_bp.url_prefix == "/library"


class TestNormalizeVectorsHandling:
    """Tests for normalize_vectors string/bool handling."""

    def test_normalize_vectors_string_true(self):
        """Test normalize_vectors string 'true' is converted to bool."""
        from local_deep_research.research_library.routes.rag_routes import (
            get_rag_service,
        )

        mock_settings = Mock()
        mock_settings.get_setting.side_effect = lambda key, default=None: {
            "local_search_embedding_model": "test-model",
            "local_search_embedding_provider": "sentence_transformers",
            "local_search_chunk_size": "1000",
            "local_search_chunk_overlap": "200",
            "local_search_splitter_type": "recursive",
            "local_search_text_separators": "[]",
            "local_search_distance_metric": "cosine",
            "local_search_normalize_vectors": "true",  # String!
            "local_search_index_type": "flat",
        }.get(key, default)
        # get_bool_setting is used for normalize_vectors
        mock_settings.get_bool_setting.return_value = True

        with patch(
            "local_deep_research.research_library.services.rag_service_factory.get_settings_manager",
            return_value=mock_settings,
        ):
            with patch(
                "local_deep_research.research_library.routes.rag_routes.session",
                {"username": "testuser"},
            ):
                with patch(
                    "local_deep_research.research_library.services.rag_service_factory.get_user_db_session"
                ) as mock_ctx:
                    mock_ctx.return_value.__enter__ = Mock(
                        return_value=MagicMock()
                    )
                    mock_ctx.return_value.__exit__ = Mock(return_value=False)

                    with patch(
                        "local_deep_research.research_library.services.rag_service_factory.LibraryRAGService"
                    ) as mock_rag:
                        mock_service = Mock()
                        mock_rag.return_value = mock_service

                        get_rag_service()

                        call_kwargs = mock_rag.call_args[1]
                        assert call_kwargs["normalize_vectors"] is True

    def test_normalize_vectors_string_false(self):
        """Test normalize_vectors string 'false' is converted to bool."""
        from local_deep_research.research_library.routes.rag_routes import (
            get_rag_service,
        )

        mock_settings = Mock()
        mock_settings.get_setting.side_effect = lambda key, default=None: {
            "local_search_embedding_model": "test-model",
            "local_search_embedding_provider": "sentence_transformers",
            "local_search_chunk_size": "1000",
            "local_search_chunk_overlap": "200",
            "local_search_splitter_type": "recursive",
            "local_search_text_separators": "[]",
            "local_search_distance_metric": "cosine",
            "local_search_normalize_vectors": "false",  # String!
            "local_search_index_type": "flat",
        }.get(key, default)
        # get_bool_setting is used for normalize_vectors
        mock_settings.get_bool_setting.return_value = False

        with patch(
            "local_deep_research.research_library.services.rag_service_factory.get_settings_manager",
            return_value=mock_settings,
        ):
            with patch(
                "local_deep_research.research_library.routes.rag_routes.session",
                {"username": "testuser"},
            ):
                with patch(
                    "local_deep_research.research_library.services.rag_service_factory.get_user_db_session"
                ) as mock_ctx:
                    mock_ctx.return_value.__enter__ = Mock(
                        return_value=MagicMock()
                    )
                    mock_ctx.return_value.__exit__ = Mock(return_value=False)

                    with patch(
                        "local_deep_research.research_library.services.rag_service_factory.LibraryRAGService"
                    ) as mock_rag:
                        mock_service = Mock()
                        mock_rag.return_value = mock_service

                        get_rag_service()

                        call_kwargs = mock_rag.call_args[1]
                        assert call_kwargs["normalize_vectors"] is False


class TestRagApiRoutes:
    """Tests for RAG API routes."""

    def test_get_current_settings_route(self):
        """Test /api/rag/settings GET endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/rag/settings")
            assert response.status_code == 401, response.status_code

    def test_test_embedding_route(self):
        """Test /api/rag/test-embedding POST endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/test-embedding",
                json={"text": "test text"},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_get_available_models_route(self):
        """Test /api/rag/models GET endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/rag/models")
            assert response.status_code == 401, response.status_code

    def test_get_index_info_route(self):
        """Test /api/rag/info GET endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/rag/info")
            assert response.status_code == 401, response.status_code

    def test_get_rag_stats_route(self):
        """Test /api/rag/stats GET endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/rag/stats")
            assert response.status_code == 401, response.status_code

    def test_get_supported_formats_route(self):
        """Test /api/config/supported-formats GET endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: DELETE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/config/supported-formats")
            # 500 is acceptable in isolated test (login redirect fails without auth blueprint)
            assert response.status_code == 401, response.status_code


class TestRagIndexRoutes:
    """Tests for RAG indexing routes."""

    def test_index_document_route(self):
        """Test /api/rag/index-document POST endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/index-document",
                json={"document_id": "doc123"},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_remove_document_route(self):
        """Test /api/rag/remove-document POST endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/remove-document",
                json={"document_id": "doc123"},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_index_research_route(self):
        """Test /api/rag/index-research POST endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/index-research",
                json={"research_id": "research123"},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_index_all_route(self):
        """Test /api/rag/index-all GET endpoint exists."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/rag/index-all")
            assert response.status_code == 401, response.status_code


class TestRagCollectionRoutes:
    """Tests for RAG collection routes."""

    def test_get_collections_route(self):
        """Test /api/collections GET endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/collections")
            assert response.status_code == 401, response.status_code

    def test_create_collection_route(self):
        """Test /api/collections POST endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections",
                json={"name": "Test Collection"},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_update_collection_route(self):
        """Test /api/collections/<id> PUT endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.put(
                "/library/api/collections/collection123",
                json={"name": "Updated Collection"},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code


class TestRagPageRoutes:
    """Tests for RAG page routes."""

    def test_embedding_settings_page_route(self):
        """Test /embedding-settings page route exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/embedding-settings")
            assert response.status_code == 302, response.status_code

    def test_collections_page_route(self):
        """Test /collections page route exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/collections")
            assert response.status_code == 302, response.status_code

    def test_collection_details_page_route(self):
        """Test /collections/<id> page route exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/collections/collection123")
            assert response.status_code == 302, response.status_code

    def test_collection_create_page_route(self):
        """Test /collections/create page route exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/collections/create")
            assert response.status_code == 302, response.status_code


class TestRagBackgroundIndexRoutes:
    """Tests for RAG background indexing routes."""

    def test_start_background_index_route(self):
        """Test /api/collections/<id>/index/background POST endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections/collection123/index/background"
            )
            assert response.status_code == 404, response.status_code

    def test_get_index_status_route(self):
        """Test /api/collections/<id>/index/status GET endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get(
                "/library/api/collections/collection123/index/status"
            )
            assert response.status_code == 401, response.status_code

    def test_cancel_indexing_route(self):
        """Test /api/collections/<id>/index/cancel POST endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections/collection123/index/cancel"
            )
            assert response.status_code == 401, response.status_code


class TestRagUploadRoutes:
    """Tests for RAG upload routes."""

    def test_upload_to_collection_route(self):
        """Test /api/collections/<id>/upload POST endpoint exists."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            # Test without file (will likely fail but route should exist)
            response = client.post(
                "/library/api/collections/collection123/upload"
            )
            assert response.status_code == 401, response.status_code


class TestDocumentLoaders:
    """Tests for document_loaders integration in upload workflow."""

    def test_extract_text_from_txt_file(self):
        """Test extracting text from .txt file using document_loaders."""
        from local_deep_research.document_loaders import extract_text_from_bytes

        content = b"Hello, this is a test text file."

        text = extract_text_from_bytes(content, ".txt", "test.txt")
        assert "Hello" in text

    def test_extract_text_from_md_file(self):
        """Test extracting text from .md file using document_loaders."""
        from local_deep_research.document_loaders import extract_text_from_bytes

        content = b"# Header\n\nThis is markdown content."

        text = extract_text_from_bytes(content, ".md", "test.md")
        assert "Header" in text or "markdown" in text

    def test_is_extension_supported(self):
        """Test that extension support checking works correctly."""
        from local_deep_research.document_loaders import is_extension_supported

        # Supported extensions
        assert is_extension_supported(".txt") is True
        assert is_extension_supported(".pdf") is True
        assert is_extension_supported(".json") is True
        assert is_extension_supported(".yaml") is True

        # Unsupported extensions
        assert is_extension_supported(".xyz") is False
        assert is_extension_supported(".unknown") is False

    def test_extract_text_from_json_file(self):
        """Test extracting text from .json file using document_loaders."""
        from local_deep_research.document_loaders import extract_text_from_bytes

        content = (
            b'{"title": "Test Document", "content": "This is JSON content"}'
        )

        text = extract_text_from_bytes(content, ".json", "test.json")
        assert "Test Document" in text
        assert "JSON content" in text

    def test_extract_text_from_yaml_file(self):
        """Test extracting text from .yaml file using document_loaders."""
        from local_deep_research.document_loaders import extract_text_from_bytes

        content = b"title: Test YAML\ncontent: This is YAML content"

        text = extract_text_from_bytes(content, ".yaml", "test.yaml")
        assert "Test YAML" in text
        assert "YAML content" in text

    def test_extract_text_from_csv_file(self):
        """Test extracting text from .csv file using document_loaders."""
        from local_deep_research.document_loaders import extract_text_from_bytes

        content = b"name,value\ntest,123\ndata,456"

        text = extract_text_from_bytes(content, ".csv", "test.csv")
        assert "test" in text
        assert "123" in text or "data" in text

    def test_extract_text_from_html_file(self):
        """Test extracting text from .html file using document_loaders."""
        from local_deep_research.document_loaders import extract_text_from_bytes

        content = (
            b"<html><body><h1>Title</h1><p>Paragraph content</p></body></html>"
        )

        text = extract_text_from_bytes(content, ".html", "test.html")
        assert "Title" in text or "Paragraph" in text

    def test_extract_text_extension_case_insensitive(self):
        """Test that extraction works with uppercase extensions."""
        from local_deep_research.document_loaders import extract_text_from_bytes

        content = b"Hello World"

        # Should work with uppercase extension
        text = extract_text_from_bytes(content, ".TXT", "test.TXT")
        assert "Hello" in text

    def test_extract_text_extension_without_dot(self):
        """Test that extraction works without leading dot."""
        from local_deep_research.document_loaders import extract_text_from_bytes

        content = b"Hello World"

        # Should work without leading dot
        text = extract_text_from_bytes(content, "txt", "test.txt")
        assert "Hello" in text

    def test_extract_text_unsupported_returns_none(self):
        """Test that unsupported extension returns None."""
        from local_deep_research.document_loaders import extract_text_from_bytes

        content = b"Some content"

        # Unsupported extension should return None
        text = extract_text_from_bytes(content, ".xyz", "test.xyz")
        assert text is None

    def test_extract_text_empty_content(self):
        """Test extraction with empty content."""
        from local_deep_research.document_loaders import extract_text_from_bytes

        content = b""

        text = extract_text_from_bytes(content, ".txt", "empty.txt")
        # Should return empty string or None for empty content
        assert text == "" or text is None

    def test_extract_text_unicode_content(self):
        """Test extraction with unicode content."""
        from local_deep_research.document_loaders import extract_text_from_bytes

        content = "Hello 世界 🌍 émojis".encode("utf-8")

        text = extract_text_from_bytes(content, ".txt", "unicode.txt")
        assert "Hello" in text
        assert "世界" in text


class TestSupportedFormatsEndpoint:
    """Tests for /api/config/supported-formats endpoint."""

    def test_get_supported_extensions_returns_list(self):
        """Test that get_supported_extensions returns a list."""
        from local_deep_research.document_loaders import (
            get_supported_extensions,
        )

        extensions = get_supported_extensions()

        assert isinstance(extensions, list)
        assert len(extensions) > 0

    def test_get_supported_extensions_contains_common_formats(self):
        """Test that common formats are included."""
        from local_deep_research.document_loaders import (
            get_supported_extensions,
        )

        extensions = get_supported_extensions()

        # Common document formats should be present
        assert ".pdf" in extensions
        assert ".txt" in extensions
        assert ".md" in extensions
        assert ".html" in extensions
        assert ".docx" in extensions

    def test_get_supported_extensions_contains_data_formats(self):
        """Test that data formats are included."""
        from local_deep_research.document_loaders import (
            get_supported_extensions,
        )

        extensions = get_supported_extensions()

        # Data formats should be present
        assert ".json" in extensions
        assert ".yaml" in extensions
        assert ".yml" in extensions
        assert ".csv" in extensions
        assert ".xml" in extensions
        assert ".toml" in extensions

    def test_get_supported_extensions_contains_spreadsheet_formats(self):
        """Test that spreadsheet formats are included."""
        from local_deep_research.document_loaders import (
            get_supported_extensions,
        )

        extensions = get_supported_extensions()

        # Spreadsheet formats should be present
        assert ".xlsx" in extensions
        assert ".xls" in extensions
        assert ".csv" in extensions
        assert ".tsv" in extensions

    def test_get_supported_extensions_contains_presentation_formats(self):
        """Test that presentation formats are included."""
        from local_deep_research.document_loaders import (
            get_supported_extensions,
        )

        extensions = get_supported_extensions()

        # Presentation formats should be present. Legacy .ppt is only
        # registered when LibreOffice is installed, so only .pptx is asserted.
        assert ".pptx" in extensions

    def test_get_supported_extensions_all_start_with_dot(self):
        """Test that all extensions start with a dot."""
        from local_deep_research.document_loaders import (
            get_supported_extensions,
        )

        extensions = get_supported_extensions()

        for ext in extensions:
            assert ext.startswith("."), f"Extension {ext} should start with dot"

    def test_get_supported_extensions_all_lowercase(self):
        """Test that all extensions are lowercase."""
        from local_deep_research.document_loaders import (
            get_supported_extensions,
        )

        extensions = get_supported_extensions()

        for ext in extensions:
            assert ext == ext.lower(), f"Extension {ext} should be lowercase"

    def test_get_supported_extensions_no_duplicates(self):
        """Test that there are no duplicate extensions."""
        from local_deep_research.document_loaders import (
            get_supported_extensions,
        )

        extensions = get_supported_extensions()

        assert len(extensions) == len(set(extensions)), (
            "Extensions contain duplicates"
        )

    def test_is_extension_supported_with_dot(self):
        """Test is_extension_supported with dot prefix."""
        from local_deep_research.document_loaders import is_extension_supported

        assert is_extension_supported(".pdf") is True
        assert is_extension_supported(".json") is True
        assert is_extension_supported(".yaml") is True

    def test_is_extension_supported_without_dot(self):
        """Test is_extension_supported without dot prefix."""
        from local_deep_research.document_loaders import is_extension_supported

        # Should work without leading dot
        assert is_extension_supported("pdf") is True
        assert is_extension_supported("json") is True
        assert is_extension_supported("yaml") is True

    def test_is_extension_supported_case_insensitive(self):
        """Test is_extension_supported is case insensitive."""
        from local_deep_research.document_loaders import is_extension_supported

        assert is_extension_supported(".PDF") is True
        assert is_extension_supported(".Pdf") is True
        assert is_extension_supported(".JSON") is True
        assert is_extension_supported("YAML") is True

    def test_is_extension_supported_returns_false_for_unsupported(self):
        """Test is_extension_supported returns False for unsupported formats."""
        from local_deep_research.document_loaders import is_extension_supported

        assert is_extension_supported(".xyz") is False
        assert is_extension_supported(".unknown") is False
        assert is_extension_supported(".exe") is False
        assert is_extension_supported(".dll") is False

    def test_supported_formats_count(self):
        """The always-available formats are advertised regardless of env.

        Under honest capability detection (#4414) the registry only
        advertises a format when its real runtime dependency is present,
        so the *total* count varies by environment (office docs need
        python-docx/openpyxl/python-pptx, OCR needs the tesseract binary,
        etc.). The stable contract is the set of formats whose loaders
        have NO optional dependency — those are always registered. Assert
        that guaranteed set is present rather than a magic total that the
        old "advertise everything" registry happened to reach.
        """
        from local_deep_research.document_loaders import (
            get_supported_extensions,
        )

        extensions = set(get_supported_extensions())

        # Formats backed only by hard dependencies / pure-Python loaders —
        # registered unconditionally (see LOADER_REGISTRY base entries and
        # the ungated additions in loader_registry.py).
        always_available = {
            ".pdf",
            ".txt",
            ".md",
            ".markdown",
            ".csv",
            ".html",
            ".htm",
            ".xml",
            ".eml",
            ".mht",
            ".mhtml",
            ".enex",
            ".tsv",
            ".json",
            ".yaml",
            ".yml",
            ".ipynb",
            ".toml",
        }

        missing = always_available - extensions
        assert not missing, (
            f"Guaranteed formats missing from registry: {sorted(missing)}"
        )

    def test_accept_string_generation(self):
        """Test that accept string can be generated from extensions."""
        from local_deep_research.document_loaders import (
            get_supported_extensions,
        )

        extensions = get_supported_extensions()
        accept_string = ",".join(sorted(extensions))

        # Accept string should contain common formats
        assert ".pdf" in accept_string
        assert ".json" in accept_string
        assert ".yaml" in accept_string

        # Should be comma-separated
        assert "," in accept_string

        # Should not have spaces
        assert " " not in accept_string


class TestUploadWithDocumentLoaders:
    """Tests for upload workflow integration with document_loaders."""

    def test_upload_validates_extension_before_extraction(self):
        """Test that upload validates extension is supported."""
        from local_deep_research.document_loaders import is_extension_supported

        # Simulate what the upload route does
        filename = "test.xyz"
        from pathlib import Path

        file_extension = Path(filename).suffix.lower()

        # Should return False for unsupported extension
        assert is_extension_supported(file_extension) is False

    def test_upload_extracts_text_for_supported_extension(self):
        """Test that upload extracts text for supported extensions."""
        from local_deep_research.document_loaders import (
            extract_text_from_bytes,
            is_extension_supported,
        )

        # Simulate what the upload route does
        filename = "document.txt"
        file_content = b"This is the document content."
        from pathlib import Path

        file_extension = Path(filename).suffix.lower()

        # Should be supported
        assert is_extension_supported(file_extension) is True

        # Should extract text
        text = extract_text_from_bytes(file_content, file_extension, filename)
        assert "document content" in text

    def test_upload_handles_json_files(self):
        """Test that upload correctly handles JSON files."""
        from local_deep_research.document_loaders import (
            extract_text_from_bytes,
            is_extension_supported,
        )
        from pathlib import Path

        filename = "data.json"
        file_content = b'{"name": "Test", "description": "A test document"}'
        file_extension = Path(filename).suffix.lower()

        assert is_extension_supported(file_extension) is True

        text = extract_text_from_bytes(file_content, file_extension, filename)
        assert "Test" in text
        assert "test document" in text

    def test_upload_handles_yaml_files(self):
        """Test that upload correctly handles YAML files."""
        from local_deep_research.document_loaders import (
            extract_text_from_bytes,
            is_extension_supported,
        )
        from pathlib import Path

        filename = "config.yaml"
        file_content = b"name: Configuration\nsetting: value"
        file_extension = Path(filename).suffix.lower()

        assert is_extension_supported(file_extension) is True

        text = extract_text_from_bytes(file_content, file_extension, filename)
        assert "Configuration" in text

    def test_upload_handles_yml_extension(self):
        """Test that upload correctly handles .yml extension."""
        from local_deep_research.document_loaders import (
            extract_text_from_bytes,
            is_extension_supported,
        )
        from pathlib import Path

        filename = "config.yml"
        file_content = b"title: YAML with yml extension"
        file_extension = Path(filename).suffix.lower()

        assert is_extension_supported(file_extension) is True

        text = extract_text_from_bytes(file_content, file_extension, filename)
        assert "yml extension" in text

    def test_upload_handles_markdown_extension(self):
        """Test that upload correctly handles .markdown extension."""
        from local_deep_research.document_loaders import (
            extract_text_from_bytes,
            is_extension_supported,
        )
        from pathlib import Path

        filename = "readme.markdown"
        file_content = (
            b"# Readme\n\nThis is a markdown file with .markdown extension"
        )
        file_extension = Path(filename).suffix.lower()

        assert is_extension_supported(file_extension) is True

        text = extract_text_from_bytes(file_content, file_extension, filename)
        assert "Readme" in text or "markdown" in text

    def test_upload_error_message_for_unsupported_format(self):
        """Test that proper error is returned for unsupported format."""
        from local_deep_research.document_loaders import is_extension_supported
        from pathlib import Path

        filename = "document.unsupported"
        file_extension = Path(filename).suffix.lower()

        # Simulate what the upload route does
        if not is_extension_supported(file_extension):
            error = {
                "filename": filename,
                "error": f"Unsupported format: {file_extension}",
            }
            assert error["error"] == "Unsupported format: .unsupported"


class TestSupportedFormatsAPIEndpoint:
    """Integration tests for the /api/config/supported-formats endpoint."""

    @pytest.fixture
    def app(self):
        """Create a test Flask app with the rag blueprint."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.secret_key = "test-secret-key"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)
        return app

    @pytest.fixture
    def client(self, app):
        """Create a test client."""
        return app.test_client()

    def test_endpoint_requires_authentication(self, client):
        """Test that endpoint requires authentication."""
        response = client.get("/library/api/config/supported-formats")
        # Should redirect to login (302) or fail (500 if auth blueprint missing)
        assert response.status_code == 401, response.status_code

    def test_endpoint_returns_json_when_authenticated(self, client, app):
        """Test endpoint returns proper JSON when authenticated."""
        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            # Mock the database connection check
            mock_db_manager.connections = {"testuser": Mock()}

            with client.session_transaction() as sess:
                sess["username"] = "testuser"

            response = client.get("/library/api/config/supported-formats")

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

            # Verify response structure
            assert "extensions" in data
            assert "accept_string" in data
            assert "count" in data

            # Verify extensions is a list
            assert isinstance(data["extensions"], list)
            assert len(data["extensions"]) > 0

            # Verify count matches
            assert data["count"] == len(data["extensions"])

            # Verify accept_string is comma-separated
            assert "," in data["accept_string"]

    def test_endpoint_returns_sorted_extensions(self, client, app):
        """Test that extensions are returned in sorted order."""
        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.connections = {"testuser": Mock()}

            with client.session_transaction() as sess:
                sess["username"] = "testuser"

            response = client.get("/library/api/config/supported-formats")

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

            extensions = data["extensions"]
            assert extensions == sorted(extensions)

    def test_endpoint_includes_common_formats(self, client, app):
        """Test that common formats are included in response."""
        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.connections = {"testuser": Mock()}

            with client.session_transaction() as sess:
                sess["username"] = "testuser"

            response = client.get("/library/api/config/supported-formats")

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

            extensions = data["extensions"]

            # Check common formats
            assert ".pdf" in extensions
            assert ".txt" in extensions
            assert ".json" in extensions
            assert ".yaml" in extensions
            assert ".csv" in extensions
            assert ".html" in extensions
            assert ".docx" in extensions

    def test_accept_string_matches_extensions(self, client, app):
        """Test that accept_string contains all extensions."""
        with patch(
            "local_deep_research.web.auth.decorators.db_manager"
        ) as mock_db_manager:
            mock_db_manager.connections = {"testuser": Mock()}

            with client.session_transaction() as sess:
                sess["username"] = "testuser"

            response = client.get("/library/api/config/supported-formats")

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

            # All extensions should be in accept_string
            for ext in data["extensions"]:
                assert ext in data["accept_string"]


# ============= Extended Tests for Phase 3.2 Coverage =============


class TestConfigureRagEndpoint:
    """Extended tests for RAG configuration endpoint."""

    def test_configure_rag_missing_embedding_model(self):
        """Test configure RAG with missing embedding_model."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/configure",
                json={
                    "embedding_provider": "sentence_transformers",
                    "chunk_size": 1000,
                    "chunk_overlap": 200,
                },
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_configure_rag_missing_provider(self):
        """Test configure RAG with missing embedding_provider."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/configure",
                json={
                    "embedding_model": "all-MiniLM-L6-v2",
                    "chunk_size": 1000,
                    "chunk_overlap": 200,
                },
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_configure_rag_with_all_advanced_settings(self):
        """Test configure RAG with all advanced settings."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/configure",
                json={
                    "embedding_model": "all-MiniLM-L6-v2",
                    "embedding_provider": "sentence_transformers",
                    "chunk_size": 500,
                    "chunk_overlap": 100,
                    "splitter_type": "sentence",
                    "text_separators": ["\n\n", "\n", ". "],
                    "distance_metric": "euclidean",
                    "normalize_vectors": False,
                    "index_type": "hnsw",
                    "collection_id": "test_collection",
                },
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code


class TestIndexDocumentEndpoint:
    """Extended tests for index document endpoint."""

    def test_index_document_missing_text_doc_id(self):
        """Test index document without text_doc_id."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/index-document",
                json={},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_index_document_with_force_reindex(self):
        """Test index document with force_reindex flag."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/index-document",
                json={
                    "text_doc_id": "doc123",
                    "force_reindex": True,
                    "collection_id": "coll123",
                },
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code


class TestRemoveDocumentEndpoint:
    """Extended tests for remove document endpoint."""

    def test_remove_document_missing_text_doc_id(self):
        """Test remove document without text_doc_id."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/remove-document",
                json={},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code


class TestIndexResearchEndpoint:
    """Extended tests for index research endpoint."""

    def test_index_research_missing_research_id(self):
        """Test index research without research_id."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/index-research",
                json={},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code


class TestIndexLocalEndpoint:
    """Extended tests for index local library endpoint."""

    def test_index_local_missing_path(self):
        """Test index local without path."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/rag/index-local")
            assert response.status_code == 401, response.status_code

    def test_index_local_path_traversal_attempt(self):
        """Test index local with path traversal attempt."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get(
                "/library/api/rag/index-local?path=../../etc/passwd"
            )
            assert response.status_code == 401, response.status_code

    def test_index_local_with_patterns(self):
        """Test index local with custom patterns."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get(
                "/library/api/rag/index-local?path=/tmp&patterns=*.pdf,*.txt"
            )
            assert response.status_code == 401, response.status_code


class TestGetDocumentsEndpoint:
    """Extended tests for get documents endpoint."""

    def test_get_documents_with_pagination(self):
        """Test get documents with pagination."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get(
                "/library/api/rag/documents?page=2&per_page=25"
            )
            assert response.status_code == 401, response.status_code

    def test_get_documents_filter_indexed(self):
        """Test get documents with indexed filter."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/rag/documents?filter=indexed")
            assert response.status_code == 401, response.status_code

    def test_get_documents_filter_unindexed(self):
        """Test get documents with unindexed filter."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/rag/documents?filter=unindexed")
            assert response.status_code == 401, response.status_code

    def test_get_documents_with_collection_id(self):
        """Test get documents with collection_id."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get(
                "/library/api/rag/documents?collection_id=coll123"
            )
            assert response.status_code == 401, response.status_code


class TestCollectionEndpoints:
    """Extended tests for collection management endpoints."""

    def test_create_collection_missing_name(self):
        """Test create collection without name."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections",
                json={},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_create_collection_with_all_fields(self):
        """Test create collection with all optional fields."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections",
                json={
                    "name": "Test Collection",
                    "description": "A test collection",
                    "collection_type": "research",
                },
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_get_single_collection(self):
        """Test get single collection."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/collections/coll123")
            assert response.status_code == 405, response.status_code


class TestCollectionDocumentEndpoints:
    """Extended tests for collection document management."""

    def test_add_document_to_collection(self):
        """Test adding document to collection."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections/coll123/documents",
                json={"document_id": "doc123"},
                content_type="application/json",
            )
            assert response.status_code == 405, response.status_code

    def test_remove_document_from_collection(self):
        """Test removing document from collection."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.delete(
                "/library/api/collections/coll123/documents/doc123"
            )
            assert response.status_code == 404, response.status_code

    def test_get_collection_documents(self):
        """Test getting documents in a collection."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/collections/coll123/documents")
            assert response.status_code == 401, response.status_code


class TestSearchEndpoint:
    """Extended tests for search endpoint."""

    def test_search_collection_missing_query(self):
        """Test search without query."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections/coll123/search",
                json={},
                content_type="application/json",
            )
            assert response.status_code == 404, response.status_code

    def test_search_collection_with_limit(self):
        """Test search with limit parameter."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections/coll123/search",
                json={"query": "test query", "limit": 5},
                content_type="application/json",
            )
            assert response.status_code == 404, response.status_code


class TestFileUploadEndpoint:
    """Extended tests for file upload endpoint."""

    def test_upload_pdf_file(self):
        """Test uploading a PDF file."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )
        from io import BytesIO

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            data = {"file": (BytesIO(b"%PDF-1.4 fake content"), "test.pdf")}
            response = client.post(
                "/library/api/collections/coll123/upload",
                data=data,
                content_type="multipart/form-data",
            )
            assert response.status_code == 401, response.status_code

    def test_upload_txt_file(self):
        """Test uploading a text file."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )
        from io import BytesIO

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            data = {"file": (BytesIO(b"Test text content"), "test.txt")}
            response = client.post(
                "/library/api/collections/coll123/upload",
                data=data,
                content_type="multipart/form-data",
            )
            assert response.status_code == 401, response.status_code


class TestTestEmbeddingEndpoint:
    """Extended tests for test embedding endpoint."""

    def test_test_embedding_missing_provider(self):
        """Test embedding test without provider."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/test-embedding",
                json={"model": "all-MiniLM-L6-v2"},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_test_embedding_missing_model(self):
        """Test embedding test without model."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/test-embedding",
                json={"provider": "sentence_transformers"},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code


class TestRagEdgeCases:
    """Extended edge case tests for RAG routes."""

    def test_very_large_chunk_size(self):
        """Test configuration with very large chunk size."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/configure",
                json={
                    "embedding_model": "model",
                    "embedding_provider": "provider",
                    "chunk_size": 999999999,
                    "chunk_overlap": 200,
                },
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_negative_chunk_size(self):
        """Test configuration with negative chunk size."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/configure",
                json={
                    "embedding_model": "model",
                    "embedding_provider": "provider",
                    "chunk_size": -100,
                    "chunk_overlap": 200,
                },
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_overlap_larger_than_chunk(self):
        """Test configuration where overlap > chunk size."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/rag/configure",
                json={
                    "embedding_model": "model",
                    "embedding_provider": "provider",
                    "chunk_size": 100,
                    "chunk_overlap": 500,
                },
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_sql_injection_in_collection_id(self):
        """Test SQL injection attempt in collection ID."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get(
                "/library/api/collections/'; DROP TABLE collections; --"
            )
            assert response.status_code == 405, response.status_code

    def test_special_chars_in_collection_name(self):
        """Test creating collection with special characters."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections",
                json={"name": "<script>alert('xss')</script>"},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_unicode_in_collection_name(self):
        """Test creating collection with unicode characters."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections",
                json={"name": "测试集合 コレクション مجموعة"},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_empty_collection_name(self):
        """Test creating collection with empty name."""
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections",
                json={"name": ""},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code

    def test_very_long_collection_name(self):
        """Test creating collection with very long name."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post(
                "/library/api/collections",
                json={"name": "a" * 10000},
                content_type="application/json",
            )
            assert response.status_code == 401, response.status_code


class TestCollectionNormalizeVectors:
    """Tests for collection normalize_vectors handling."""

    def test_collection_normalize_vectors_string_handling(self):
        """Test that collection normalize_vectors handles string values."""
        from local_deep_research.research_library.routes.rag_routes import (
            get_rag_service,
        )

        mock_settings = Mock()
        mock_settings.get_setting.side_effect = lambda key, default=None: {
            "local_search_embedding_model": "test-model",
            "local_search_embedding_provider": "sentence_transformers",
            "local_search_chunk_size": "1000",
            "local_search_chunk_overlap": "200",
            "local_search_splitter_type": "recursive",
            "local_search_text_separators": "[]",
            "local_search_distance_metric": "cosine",
            "local_search_normalize_vectors": True,
            "local_search_index_type": "flat",
        }.get(key, default)
        mock_settings.get_bool_setting.return_value = True

        mock_collection = Mock()
        mock_collection.embedding_model = "coll-model"
        mock_collection.embedding_model_type = Mock()
        mock_collection.embedding_model_type.value = "sentence_transformers"
        mock_collection.chunk_size = 500
        mock_collection.chunk_overlap = 100
        mock_collection.splitter_type = "recursive"
        mock_collection.text_separators = ["\n"]
        mock_collection.distance_metric = "cosine"
        mock_collection.normalize_vectors = "true"  # String value
        mock_collection.index_type = "flat"

        mock_db_session = MagicMock()
        mock_query = MagicMock()
        mock_db_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.first.return_value = mock_collection

        with patch(
            "local_deep_research.research_library.services.rag_service_factory.get_settings_manager",
            return_value=mock_settings,
        ):
            with patch(
                "local_deep_research.research_library.routes.rag_routes.session",
                {"username": "testuser"},
            ):
                with patch(
                    "local_deep_research.research_library.services.rag_service_factory.get_user_db_session"
                ) as mock_ctx:
                    mock_ctx.return_value.__enter__ = Mock(
                        return_value=mock_db_session
                    )
                    mock_ctx.return_value.__exit__ = Mock(return_value=False)

                    with patch(
                        "local_deep_research.research_library.services.rag_service_factory.LibraryRAGService"
                    ) as mock_rag:
                        mock_service = Mock()
                        mock_rag.return_value = mock_service

                        get_rag_service(collection_id="col123")

                        call_kwargs = mock_rag.call_args[1]
                        # String "true" should be converted to boolean True
                        assert call_kwargs["normalize_vectors"] is True


class TestIndexAllStreamingResponse:
    """Tests for index-all SSE streaming response."""

    def test_index_all_returns_sse_response(self):
        """Test that index-all returns SSE response."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.get("/library/api/rag/index-all")
            # Should return 200 with text/event-stream or require auth
            assert response.status_code == 401, response.status_code
            if response.status_code == 200:
                assert "text/event-stream" in response.content_type


class TestAutoIndexTrigger:
    """Tests for auto-index trigger endpoint."""

    def test_trigger_auto_index(self):
        """Test triggering auto-index."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from flask import Flask
        from local_deep_research.research_library.routes.rag_routes import (
            rag_bp,
        )

        app = Flask(__name__)
        app.config["SECRET_KEY"] = "test-secret"
        app.register_blueprint(rag_bp)
        app.register_blueprint(auth_bp)

        with app.test_client() as client:
            response = client.post("/library/api/rag/trigger-auto-index")
            assert response.status_code == 404, response.status_code


class TestSettingsManagerImportCompatibility:
    """
    Tests to verify that rag_routes imports the correct SettingsManager.

    Issue #1877: The code was importing SettingsManager from settings.manager
    which does NOT have get_bool_setting(), causing:
    'SettingsManager' object has no attribute 'get_bool_setting'

    The fix changes imports to settings.manager which has both:
    - get_bool_setting()
    - get_settings_snapshot()

    These tests prevent regression by verifying the imported class has all
    required methods.
    """

    def test_settings_manager_has_get_bool_setting(self):
        """
        Verify settings.manager.SettingsManager has get_bool_setting.

        This method is called at lines 133, 2185, and 2309 in rag_routes.py.
        """
        from local_deep_research.settings.manager import (
            SettingsManager,
        )

        manager = SettingsManager()
        assert hasattr(manager, "get_bool_setting"), (
            "settings.manager.SettingsManager must have "
            "get_bool_setting method for rag_routes.py compatibility"
        )
        assert callable(manager.get_bool_setting)

    def test_settings_manager_has_get_settings_snapshot(self):
        """
        Verify settings.manager.SettingsManager has get_settings_snapshot.

        This method is called at line 2193 in rag_routes.py.
        """
        from local_deep_research.settings.manager import (
            SettingsManager,
        )

        manager = SettingsManager()
        assert hasattr(manager, "get_settings_snapshot"), (
            "settings.manager.SettingsManager must have "
            "get_settings_snapshot method for rag_routes.py compatibility"
        )
        assert callable(manager.get_settings_snapshot)


class TestBackgroundThreadSettingsManagerUsage:
    """
    Tests for SettingsManager usage in background thread functions.

    The functions _get_rag_service_for_thread() and trigger_auto_index()
    run outside Flask context and directly instantiate SettingsManager.
    These tests verify the usage patterns work correctly.
    """

    def test_trigger_auto_index_uses_get_bool_setting(self):
        """
        Test that trigger_auto_index correctly uses get_bool_setting.

        This verifies the pattern at line 2309 works:
        settings.get_bool_setting("research_library.auto_index_enabled", True)
        """
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from local_deep_research.research_library.routes.rag_routes import (
            trigger_auto_index,
        )

        mock_settings = Mock()
        mock_settings.get_bool_setting.return_value = (
            False  # Disable auto-index
        )

        mock_db_session = MagicMock()

        # Patch at the source module (get_user_db_session is imported inside function)
        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_ctx:
            mock_ctx.return_value.__enter__ = Mock(return_value=mock_db_session)
            mock_ctx.return_value.__exit__ = Mock(return_value=False)

            # Patch SettingsManager where it's used in rag_routes module
            with patch(
                "local_deep_research.research_library.routes.rag_routes.SettingsManager",
                return_value=mock_settings,
            ):
                trigger_auto_index(
                    document_ids=["doc1"],
                    collection_id="col1",
                    username="testuser",
                    db_password="testpass",
                )

        # Verify get_bool_setting was called correctly
        mock_settings.get_bool_setting.assert_called_with(
            "research_library.auto_index_enabled", True
        )

    def test_trigger_auto_index_skips_when_disabled(self):
        """Test auto-indexing is skipped when setting returns False."""
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from local_deep_research.research_library.routes.rag_routes import (
            trigger_auto_index,
        )

        mock_settings = Mock()
        mock_settings.get_bool_setting.return_value = False  # Disabled

        mock_db_session = MagicMock()

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_ctx:
            mock_ctx.return_value.__enter__ = Mock(return_value=mock_db_session)
            mock_ctx.return_value.__exit__ = Mock(return_value=False)

            with patch(
                "local_deep_research.research_library.routes.rag_routes.SettingsManager",
                return_value=mock_settings,
            ):
                with patch(
                    "local_deep_research.research_library.routes.rag_routes._get_auto_index_executor"
                ) as mock_executor:
                    trigger_auto_index(
                        document_ids=["doc1"],
                        collection_id="col1",
                        username="testuser",
                        db_password="testpass",
                    )

                    # Executor should NOT be called when auto-index is disabled
                    mock_executor.assert_not_called()

    def test_trigger_auto_index_empty_documents(self):
        """Test trigger_auto_index returns early with empty document list."""
        from local_deep_research.research_library.routes.rag_routes import (
            trigger_auto_index,
        )

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_ctx:
            # Should return early before even checking settings
            trigger_auto_index(
                document_ids=[],  # Empty!
                collection_id="col1",
                username="testuser",
                db_password="testpass",
            )

            # Should not create a session
            mock_ctx.assert_not_called()

    def test_get_rag_service_for_thread_mock_setup(self):
        """
        Test that _get_rag_service_for_thread can be called with proper mocks.

        This verifies the function signature and basic mock compatibility.
        """
        # audit: PUNCHLIST reviewed 2026-05 — issue resolved by prior PR (recommendation: REWRITE).
        from local_deep_research.research_library.routes.rag_routes import (
            _get_rag_service_for_thread,
        )

        # Create comprehensive mock for SettingsManager
        mock_settings = Mock()
        mock_settings.get_setting.return_value = "test-value"
        mock_settings.get_bool_setting.return_value = True
        mock_settings.get_settings_snapshot.return_value = {
            "local_search_embedding_model": "test-model",
            "local_search_embedding_provider": "sentence_transformers",
        }

        # Mock collection
        mock_collection = Mock()
        mock_collection.embedding_model = "test-model"
        mock_collection.embedding_model_type = Mock()
        mock_collection.embedding_model_type.value = "sentence_transformers"
        mock_collection.chunk_size = 1000
        mock_collection.chunk_overlap = 200
        mock_collection.splitter_type = "recursive"
        mock_collection.text_separators = None
        mock_collection.distance_metric = "cosine"
        mock_collection.normalize_vectors = True
        mock_collection.index_type = "flat"

        mock_db_session = MagicMock()
        mock_query = MagicMock()
        mock_db_session.query.return_value = mock_query
        mock_query.filter_by.return_value = mock_query
        mock_query.first.return_value = mock_collection

        with patch(
            "local_deep_research.database.session_context.get_user_db_session"
        ) as mock_ctx:
            mock_ctx.return_value.__enter__ = Mock(return_value=mock_db_session)
            mock_ctx.return_value.__exit__ = Mock(return_value=False)

            with patch(
                "local_deep_research.settings.manager.SettingsManager",
                return_value=mock_settings,
            ):
                with patch(
                    "local_deep_research.research_library.routes.rag_routes.LibraryRAGService"
                ) as mock_rag:
                    mock_rag.return_value = Mock()

                    with patch(
                        "local_deep_research.web_search_engines.engines.local_embedding_manager.LocalEmbeddingManager"
                    ):
                        try:
                            _get_rag_service_for_thread(
                                username="testuser",
                                db_password="testpass",
                                collection_id="test-collection",
                            )
                            # If we get here without AttributeError, the imports are correct
                        except Exception as e:
                            # Some exceptions are expected due to incomplete mocking
                            # But AttributeError for missing methods should not occur
                            assert (
                                "has no attribute 'get_bool_setting'"
                                not in str(e)
                            ), (
                                "SettingsManager missing get_bool_setting - "
                                "wrong class imported?"
                            )
                            assert (
                                "has no attribute 'get_settings_snapshot'"
                                not in str(e)
                            ), (
                                "SettingsManager missing get_settings_snapshot - "
                                "wrong class imported?"
                            )


class TestEmbeddingProviderAvailability:
    """Tests for embedding provider dropdown always showing all providers.

    Even when a provider is unreachable (e.g. Ollama at wrong URL), it
    should still appear in the dropdown so users can configure its settings.
    The 'available' flag indicates whether the provider is reachable.
    """

    def test_unavailable_provider_included_in_options(self):
        """Unavailable providers should appear in provider_options with available=False."""
        from unittest.mock import Mock

        # Create mock provider classes
        mock_available_provider = Mock()
        mock_available_provider.is_available.return_value = True
        mock_available_provider.get_available_models.return_value = [
            {"value": "model-a", "label": "Model A"}
        ]

        mock_unavailable_provider = Mock()
        mock_unavailable_provider.is_available.return_value = False
        # get_available_models should NOT be called for unavailable providers

        provider_classes = {
            "sentence_transformers": mock_available_provider,
            "ollama": mock_unavailable_provider,
        }

        provider_labels = {
            "sentence_transformers": "Sentence Transformers (Local)",
            "ollama": "Ollama (Local)",
        }

        # Reproduce the logic from get_available_models
        provider_options = []
        providers = {}

        for provider_key, provider_class in provider_classes.items():
            available = provider_class.is_available({})
            provider_options.append(
                {
                    "value": provider_key,
                    "label": provider_labels.get(provider_key, provider_key),
                    "available": available,
                }
            )
            if available:
                models = provider_class.get_available_models({})
                providers[provider_key] = [
                    {
                        "value": m["value"],
                        "label": m["label"],
                        "provider": provider_key,
                    }
                    for m in models
                ]
            else:
                providers[provider_key] = []

        # Both providers should be in options
        assert len(provider_options) == 2

        st_option = next(
            p for p in provider_options if p["value"] == "sentence_transformers"
        )
        ollama_option = next(
            p for p in provider_options if p["value"] == "ollama"
        )

        assert st_option["available"] is True
        assert ollama_option["available"] is False
        assert ollama_option["label"] == "Ollama (Local)"

        # Available provider should have models, unavailable should have empty list
        assert len(providers["sentence_transformers"]) == 1
        assert providers["ollama"] == []

        # get_available_models should NOT have been called for unavailable provider
        mock_unavailable_provider.get_available_models.assert_not_called()

    def test_all_providers_unavailable_still_shown(self):
        """Even when all providers are unavailable, they should all be listed."""
        from unittest.mock import Mock

        mock_provider_a = Mock()
        mock_provider_a.is_available.return_value = False

        mock_provider_b = Mock()
        mock_provider_b.is_available.return_value = False

        provider_classes = {
            "provider_a": mock_provider_a,
            "provider_b": mock_provider_b,
        }

        provider_options = []
        for provider_key, provider_class in provider_classes.items():
            available = provider_class.is_available({})
            provider_options.append(
                {
                    "value": provider_key,
                    "label": provider_key,
                    "available": available,
                }
            )

        assert len(provider_options) == 2
        assert all(not p["available"] for p in provider_options)

    def test_available_flag_is_boolean(self):
        """The 'available' field should be a proper boolean, not truthy/falsy."""
        from unittest.mock import Mock

        mock_provider = Mock()
        mock_provider.is_available.return_value = True

        available = mock_provider.is_available({})
        option = {"value": "test", "label": "Test", "available": available}

        assert option["available"] is True
        assert isinstance(option["available"], bool)
