# allow: no-sut-import — black-box HTTP test; drives real routes through the Flask test client
"""Tests for index_local_library and view_document_chunks in rag_routes.py."""

from unittest.mock import patch, MagicMock

RAG_PREFIX = "/library"


class TestIndexLocalLibrary:
    """Tests for /api/rag/index-local SSE endpoint."""

    def test_requires_authentication(self, client):
        response = client.get(
            f"{RAG_PREFIX}/api/rag/index-local?path=/tmp/docs"
        )
        assert response.status_code in [401, 302]

    def test_missing_path_returns_400(self, authenticated_client):
        response = authenticated_client.get(f"{RAG_PREFIX}/api/rag/index-local")
        assert response.status_code == 400
        data = response.get_json()
        assert data["success"] is False
        assert "path" in data["error"].lower()

    def test_traversal_glob_pattern_returns_400(self, authenticated_client):
        """A glob pattern that escapes the base dir is rejected before any
        path handling, so no traversal read can occur."""
        response = authenticated_client.get(
            f"{RAG_PREFIX}/api/rag/index-local"
            "?path=/tmp/docs&patterns=../../../etc/passwd"
        )
        assert response.status_code == 400
        data = response.get_json()
        assert data["success"] is False
        assert "pattern" in data["error"].lower()

    def test_absolute_glob_pattern_returns_400(self, authenticated_client):
        """An absolute pattern is rejected: pathlib's "/" resets to an absolute
        path, so ``base / "/etc/*"`` would otherwise escape the folder."""
        response = authenticated_client.get(
            f"{RAG_PREFIX}/api/rag/index-local?path=/tmp/docs&patterns=/etc/*"
        )
        assert response.status_code == 400
        data = response.get_json()
        assert data["success"] is False
        assert "pattern" in data["error"].lower()

    def test_empty_patterns_returns_400(self, authenticated_client):
        """A patterns value that is only commas/whitespace leaves nothing to
        match and is rejected rather than silently indexing everything."""
        response = authenticated_client.get(
            f"{RAG_PREFIX}/api/rag/index-local?path=/tmp/docs&patterns=,%20,"
        )
        assert response.status_code == 400
        data = response.get_json()
        assert data["success"] is False
        assert "pattern" in data["error"].lower()

    def test_one_bad_pattern_in_list_rejects_whole_request(
        self, authenticated_client
    ):
        """A single unsafe pattern alongside valid ones rejects the request
        (the validation is an all()-gate, not a silent filter)."""
        response = authenticated_client.get(
            f"{RAG_PREFIX}/api/rag/index-local"
            "?path=/tmp/docs&patterns=*.pdf,../etc/passwd"
        )
        assert response.status_code == 400
        data = response.get_json()
        assert data["success"] is False
        assert "pattern" in data["error"].lower()

    def test_valid_custom_patterns_pass_the_allowlist(
        self, authenticated_client
    ):
        """Realistic custom globs must NOT be rejected by the allowlist —
        guards against a future over-tightening of the regex. The request may
        still fail because the path does not exist, but never for a pattern
        reason."""
        for pattern in ("data_*.csv", "*.PDF", "report-[0-9].txt"):
            response = authenticated_client.get(
                f"{RAG_PREFIX}/api/rag/index-local"
                f"?path=/tmp/docs&patterns={pattern}"
            )
            error = (response.get_json() or {}).get("error") or ""
            assert "pattern" not in error.lower(), (
                f"{pattern!r} was wrongly rejected as an invalid pattern"
            )

    @patch(
        "local_deep_research.research_library.routes.rag_routes.PathValidator"
    )
    def test_invalid_path_returns_400(
        self, mock_validator, authenticated_client
    ):
        mock_validator.validate_local_filesystem_path.side_effect = ValueError(
            "blocked"
        )

        response = authenticated_client.get(
            f"{RAG_PREFIX}/api/rag/index-local?path=/etc/passwd"
        )
        assert response.status_code == 400
        data = response.get_json()
        assert data["success"] is False

    @patch(
        "local_deep_research.research_library.routes.rag_routes.PathValidator"
    )
    def test_invalid_path_does_not_log_submitted_path(
        self, mock_validator, authenticated_client
    ):
        """On a path-validation failure the submitted path must not reach the
        logs (it can contain a username); only the generic reason is logged."""
        from loguru import logger

        mock_validator.validate_local_filesystem_path.side_effect = ValueError(
            "Cannot access system directories"
        )
        secret_path = "/home/alice-secret-9f3a/private"

        # The package disables loguru by default; enable it so the warning
        # emitted from the route reaches our sink.
        logger.enable("local_deep_research")
        logged: list[str] = []
        sink_id = logger.add(
            lambda m: logged.append(m.record["message"]), level="WARNING"
        )
        try:
            response = authenticated_client.get(
                f"{RAG_PREFIX}/api/rag/index-local?path={secret_path}"
            )
        finally:
            logger.remove(sink_id)
            logger.disable("local_deep_research")

        assert response.status_code == 400
        failed = [m for m in logged if "Path validation failed" in m]
        assert failed, "expected a path-validation-failure log"
        # the submitted path (which can contain a username) is never logged
        assert all("alice-secret-9f3a" not in m for m in failed)

    def test_indexing_complete_log_uses_folder_basename(
        self, authenticated_client, loguru_caplog_full, tmp_path
    ):
        """The success "indexing complete" log names the folder by basename,
        never the full path (which can contain a username)."""
        folder = tmp_path / "alice-secret-9f3a" / "mydocs"
        folder.mkdir(parents=True)
        (folder / "report.txt").write_text("hello", encoding="utf-8")

        rag_service = MagicMock()
        rag_service.index_local_file.return_value = {"status": "success"}

        with (
            loguru_caplog_full.at_level("DEBUG"),
            patch(
                "local_deep_research.research_library.routes.rag_routes.get_rag_service",
                return_value=rag_service,
            ),
        ):
            response = authenticated_client.get(
                f"{RAG_PREFIX}/api/rag/index-local?path={folder}&recursive=false"
            )
            response.get_data()  # drive the SSE generator

        assert response.status_code == 200
        text = loguru_caplog_full.text
        assert "alice-secret-9f3a" not in text
        assert "indexing complete for mydocs" in text

    def test_indexing_error_log_uses_file_basename(
        self, authenticated_client, loguru_caplog_full, tmp_path
    ):
        """A per-file indexing error logs the file's basename, never its full
        path (the exception block is captured too via loguru_caplog_full)."""
        folder = tmp_path / "bob-secret-1c2d" / "papers"
        folder.mkdir(parents=True)
        (folder / "thesis.txt").write_text("x", encoding="utf-8")

        rag_service = MagicMock()
        rag_service.index_local_file.side_effect = RuntimeError("boom")

        with (
            loguru_caplog_full.at_level("DEBUG"),
            patch(
                "local_deep_research.research_library.routes.rag_routes.get_rag_service",
                return_value=rag_service,
            ),
        ):
            response = authenticated_client.get(
                f"{RAG_PREFIX}/api/rag/index-local?path={folder}&recursive=false"
            )
            response.get_data()

        text = loguru_caplog_full.text
        assert "bob-secret-1c2d" not in text
        assert "Error indexing file thesis.txt" in text

    @patch(
        "local_deep_research.research_library.routes.rag_routes.PathValidator"
    )
    def test_nonexistent_path_returns_400(
        self, mock_validator, authenticated_client, tmp_path
    ):
        nonexistent = tmp_path / "nonexistent"
        mock_validator.validate_local_filesystem_path.return_value = nonexistent
        mock_validator.sanitize_for_filesystem_ops.return_value = nonexistent

        response = authenticated_client.get(
            f"{RAG_PREFIX}/api/rag/index-local?path={nonexistent}"
        )
        assert response.status_code == 400

    @patch(
        "local_deep_research.research_library.routes.rag_routes.PathValidator"
    )
    def test_file_not_directory_returns_400(
        self, mock_validator, authenticated_client, tmp_path
    ):
        file_path = tmp_path / "file.txt"
        file_path.write_text("content")
        mock_validator.validate_local_filesystem_path.return_value = file_path
        mock_validator.sanitize_for_filesystem_ops.return_value = file_path

        response = authenticated_client.get(
            f"{RAG_PREFIX}/api/rag/index-local?path={file_path}"
        )
        assert response.status_code == 400


class TestViewDocumentChunks:
    """Tests for /api/rag/document/<id>/chunks endpoint."""

    def test_requires_authentication(self, client):
        response = client.get(f"{RAG_PREFIX}/document/123/chunks")
        assert response.status_code in [401, 302]

    @patch("local_deep_research.database.session_context.get_user_db_session")
    def test_document_not_found(self, mock_session_ctx, authenticated_client):
        mock_session = MagicMock()
        mock_session.query.return_value.filter_by.return_value.first.return_value = None
        mock_session_ctx.return_value.__enter__ = MagicMock(
            return_value=mock_session
        )
        mock_session_ctx.return_value.__exit__ = MagicMock(return_value=None)

        response = authenticated_client.get(f"{RAG_PREFIX}/document/999/chunks")
        assert response.status_code == 404
