"""Tests for history_routes module - History endpoints."""

from unittest.mock import patch, MagicMock

import pytest


# History routes are registered under /history prefix
HISTORY_PREFIX = "/history"


@pytest.fixture
def client():
    """Create a test client without authentication."""
    from flask import Flask

    # Create a minimal Flask app for testing
    app = Flask(__name__)
    app.config["TESTING"] = True
    app.config["SECRET_KEY"] = "test-secret-key"
    app.config["WTF_CSRF_ENABLED"] = False

    return app.test_client()


@pytest.fixture
def authenticated_client():
    """Create a test client with authentication mocked."""
    from flask import Flask

    # Patch decorators before importing routes
    with patch(
        "local_deep_research.web.auth.decorators.login_required",
        lambda f: f,
    ):
        with patch(
            "local_deep_research.security.rate_limiter.limiter"
        ) as mock_limiter:
            mock_limiter.exempt = lambda f: f

            # Import routes with patched decorators
            import importlib
            import local_deep_research.web.routes.history_routes as history_module

            importlib.reload(history_module)

            app = Flask(__name__)
            app.config["TESTING"] = True
            app.config["SECRET_KEY"] = "test-secret-key"
            app.config["WTF_CSRF_ENABLED"] = False

            # Register blueprint
            app.register_blueprint(history_module.history_bp)

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


class TestHistoryPage:
    """Tests for /history/ endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.get(f"{HISTORY_PREFIX}/")
        # Should redirect to login or return 401
        assert response.status_code == 404, response.status_code

    def test_returns_page_when_authenticated(self, authenticated_client):
        """Should return history page when authenticated."""
        with patch(
            "local_deep_research.web.routes.history_routes.render_template_with_defaults"
        ) as mock_render:
            mock_render.return_value = "<html>History</html>"
            response = authenticated_client.get(f"{HISTORY_PREFIX}/")
            assert response.status_code == 200


class TestGetHistory:
    """Tests for /history/api endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.get(f"{HISTORY_PREFIX}/api")
        assert response.status_code == 404, response.status_code

    def test_returns_history_when_authenticated(self, authenticated_client):
        """Should return history items when authenticated."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_query = MagicMock()
            mock_query.outerjoin.return_value.group_by.return_value.order_by.return_value.limit.return_value.offset.return_value.all.return_value = []
            mock_session.query.return_value = mock_query

            response = authenticated_client.get(f"{HISTORY_PREFIX}/api")

            assert response.status_code == 200
            data = response.get_json()
            assert data["status"] == "success"
            assert "items" in data

    def test_returns_history_items(self, authenticated_client):
        """Should return formatted history items."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_research = MagicMock()
            mock_research.id = "test-id-123"
            mock_research.title = "Test Research"
            mock_research.query = "Test query"
            mock_research.mode = "quick"
            mock_research.status = "completed"
            mock_research.created_at = "2024-01-01T10:00:00"
            mock_research.completed_at = "2024-01-01T10:05:00"
            mock_research.duration_seconds = 300
            mock_research.report_path = "/path/to/report.md"
            mock_research.research_meta = {"key": "value"}
            mock_research.progress_log = []
            mock_research.chat_session_id = None

            # Set up query chain for JOIN query. The projected query
            # yields flat Rows (document_count is a labeled column), so
            # the loop iterates `for research in results`.
            mock_research.document_count = 0
            mock_query = MagicMock()
            mock_query.outerjoin.return_value.group_by.return_value.order_by.return_value.limit.return_value.offset.return_value.all.return_value = [
                mock_research
            ]
            mock_session.query.return_value = mock_query

            response = authenticated_client.get(f"{HISTORY_PREFIX}/api")

            assert response.status_code == 200
            data = response.get_json()
            assert data["status"] == "success"
            assert len(data["items"]) == 1
            assert data["items"][0]["id"] == "test-id-123"

            # Verify sensitive fields are NOT leaked in response
            item = data["items"][0]
            assert "report_path" not in item
            assert "progress_log" not in item
            assert "research_meta" not in item
            assert "settings_snapshot" not in str(item.get("metadata", {}))

    def test_query_projects_columns_not_full_entity(self, authenticated_client):
        """get_history must project only metadata columns, never the full
        ResearchHistory entity — querying the entity eagerly loads the
        large report_content Text body into memory. Regression guard for
        #4560 (a revert to query(ResearchHistory) is output-identical and
        would otherwise pass silently)."""
        from local_deep_research.database.models import ResearchHistory

        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_query = MagicMock()
            mock_query.outerjoin.return_value.group_by.return_value.order_by.return_value.limit.return_value.offset.return_value.all.return_value = []
            mock_session.query.return_value = mock_query

            response = authenticated_client.get(f"{HISTORY_PREFIX}/api")

            assert response.status_code == 200
            # Identity checks: a SQLAlchemy column's __eq__ builds a SQL
            # clause, so `in`/`==` membership tests are unsafe here.
            selected = mock_session.query.call_args.args
            assert not any(arg is ResearchHistory for arg in selected), (
                "get_history must not query the full ResearchHistory entity"
            )
            assert not any(
                arg is ResearchHistory.report_content for arg in selected
            ), "get_history must not load the report_content body"

    def test_filters_settings_snapshot_from_metadata(
        self, authenticated_client
    ):
        """Should filter settings_snapshot from metadata and only expose is_news_search."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_research = MagicMock()
            mock_research.id = "test-id-news"
            mock_research.title = "News Research"
            mock_research.query = "Latest AI news"
            mock_research.mode = "quick"
            mock_research.status = "completed"
            mock_research.created_at = "2024-01-01T10:00:00"
            mock_research.completed_at = "2024-01-01T10:05:00"
            mock_research.duration_seconds = 300
            mock_research.research_meta = {
                "is_news_search": True,
                "settings_snapshot": {"api_key": "sk-secret-key-12345"},
            }
            mock_research.chat_session_id = None

            mock_research.document_count = 3
            mock_query = MagicMock()
            mock_query.outerjoin.return_value.group_by.return_value.order_by.return_value.limit.return_value.offset.return_value.all.return_value = [
                mock_research
            ]
            mock_session.query.return_value = mock_query

            response = authenticated_client.get(f"{HISTORY_PREFIX}/api")

            assert response.status_code == 200
            data = response.get_json()
            item = data["items"][0]

            # is_news_search=True should be correctly extracted
            assert item["metadata"] == {"is_news_search": True}

            # settings_snapshot and API keys must NOT appear anywhere in the item
            item_str = str(item)
            assert "settings_snapshot" not in item_str
            assert "api_key" not in item_str
            assert "sk-secret-key-12345" not in item_str

    def test_handles_database_error(self, authenticated_client):
        """Should handle database errors gracefully."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session_ctx.return_value.__enter__ = MagicMock(
                side_effect=Exception("Database error")
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            response = authenticated_client.get(f"{HISTORY_PREFIX}/api")

            # Should return HTTP 500 with empty items + error status, matching
            # the symmetric /api/history endpoint in research_routes.py.
            assert response.status_code == 500
            data = response.get_json()
            assert data["status"] == "error"
            assert data["items"] == []


class TestGetResearchStatus:
    """Tests for /history/status/<research_id> endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.get(f"{HISTORY_PREFIX}/status/test-id")
        assert response.status_code == 404, response.status_code

    def test_returns_404_for_nonexistent(self, authenticated_client):
        """Should return 404 for non-existent research."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_query = MagicMock()
            mock_query.filter_by.return_value.first.return_value = None
            mock_session.query.return_value = mock_query

            response = authenticated_client.get(
                f"{HISTORY_PREFIX}/status/nonexistent-id"
            )

            assert response.status_code == 404
            data = response.get_json()
            assert data["status"] == "error"

    def test_returns_status_for_existing(self, authenticated_client):
        """Should return status for existing research."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_research = MagicMock()
            mock_research.id = "test-id"
            mock_research.query = "Test query"
            mock_research.mode = "quick"
            mock_research.status = "completed"
            mock_research.created_at = "2024-01-01T10:00:00"
            mock_research.completed_at = "2024-01-01T10:05:00"
            mock_research.progress_log = "[]"
            mock_research.report_path = "/path/to/report.md"

            mock_query = MagicMock()
            mock_query.filter_by.return_value.first.return_value = mock_research
            mock_session.query.return_value = mock_query

            with patch(
                "local_deep_research.web.routes.history_routes.get_active_research_snapshot"
            ) as mock_snapshot:
                mock_snapshot.return_value = None

                response = authenticated_client.get(
                    f"{HISTORY_PREFIX}/status/test-id"
                )

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


class TestGetResearchDetails:
    """Tests for /history/details/<research_id> endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.get(f"{HISTORY_PREFIX}/details/test-id")
        assert response.status_code == 404, response.status_code

    def test_returns_404_for_nonexistent(self, authenticated_client):
        """Should return 404 for non-existent research."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_query = MagicMock()
            mock_query.filter_by.return_value.first.return_value = None
            mock_session.query.return_value = mock_query

            response = authenticated_client.get(
                f"{HISTORY_PREFIX}/details/nonexistent-id"
            )

            assert response.status_code == 404

    def test_returns_details_for_existing(self, authenticated_client):
        """Should return details for existing research."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_research = MagicMock()
            mock_research.id = "test-id"
            mock_research.query = "Test query"
            mock_research.mode = "quick"
            mock_research.status = "completed"
            mock_research.created_at = "2024-01-01T10:00:00"
            mock_research.completed_at = "2024-01-01T10:05:00"

            # Create a mock object with proper id and query attributes
            mock_research_info = MagicMock()
            mock_research_info.id = "test-id"
            mock_research_info.query = "Test query"

            # Mock query to return research info first, then research object
            mock_query = MagicMock()
            mock_query.all.return_value = [mock_research_info]
            mock_query.filter_by.return_value.first.return_value = mock_research
            mock_session.query.return_value = mock_query

            with patch(
                "local_deep_research.web.routes.history_routes.get_logs_for_research"
            ) as mock_logs:
                mock_logs.return_value = []

                with patch(
                    "local_deep_research.web.routes.history_routes.get_research_strategy",
                    # autospec so the route's call is validated against the
                    # real signature (username is keyword-only and required).
                    autospec=True,
                ) as mock_strategy:
                    mock_strategy.return_value = "standard"

                    with patch(
                        "local_deep_research.web.routes.history_routes.get_active_research_snapshot"
                    ) as mock_snapshot:
                        mock_snapshot.return_value = None

                        response = authenticated_client.get(
                            f"{HISTORY_PREFIX}/details/test-id"
                        )

                        assert response.status_code == 200
                        data = response.get_json()
                        assert data["research_id"] == "test-id"
                        assert data["query"] == "Test query"
                        assert data["strategy"] == "standard"
                        # The route must scope the strategy lookup to the
                        # authenticated user's encrypted DB — never rely on
                        # get_user_db_session's implicit Flask-session
                        # fallback inside the service function.
                        mock_strategy.assert_called_once_with(
                            "test-id", username="testuser"
                        )


class TestGetReport:
    """Tests for /history/report/<research_id> endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.get(f"{HISTORY_PREFIX}/report/test-id")
        assert response.status_code == 404, response.status_code

    def test_returns_404_for_nonexistent(self, authenticated_client):
        """Should return 404 for non-existent research."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_query = MagicMock()
            mock_query.filter_by.return_value.first.return_value = None
            mock_session.query.return_value = mock_query

            response = authenticated_client.get(
                f"{HISTORY_PREFIX}/report/nonexistent-id"
            )

            assert response.status_code == 404
            data = response.get_json()
            assert data["status"] == "error"

    def test_returns_report_for_existing(self, authenticated_client):
        """Should return report for existing research."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_research = MagicMock()
            mock_research.id = "test-id"
            mock_research.query = "Test query"
            mock_research.mode = "quick"
            mock_research.created_at = "2024-01-01T10:00:00"
            mock_research.completed_at = "2024-01-01T10:05:00"
            mock_research.duration_seconds = 300
            # report_assembly_service.assemble_full_report joins string
            # parts; report_content and research_meta must be the real
            # types (str and dict-or-None) the production code expects.
            mock_research.report_content = (
                "# Test Report\n\nThis is test content."
            )
            mock_research.research_meta = {}

            mock_query = MagicMock()
            mock_query.filter_by.return_value.first.return_value = mock_research
            mock_query.filter_by.return_value.order_by.return_value.all.return_value = []
            mock_session.query.return_value = mock_query

            with patch(
                "local_deep_research.web.auth.decorators.current_user"
            ) as mock_current_user:
                mock_current_user.return_value = "testuser"

                with patch(
                    "local_deep_research.storage.get_report_storage"
                ) as mock_storage_factory:
                    mock_storage = MagicMock()
                    mock_storage.get_report_with_metadata.return_value = {
                        "content": "# Test Report\n\nThis is test content.",
                        "metadata": {"key": "value"},
                    }
                    mock_storage_factory.return_value = mock_storage

                    response = authenticated_client.get(
                        f"{HISTORY_PREFIX}/report/test-id"
                    )

                    assert response.status_code == 200
                    data = response.get_json()
                    assert data["status"] == "success"
                    assert "content" in data


class TestGetMarkdown:
    """Tests for /history/markdown/<research_id> endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.get(f"{HISTORY_PREFIX}/markdown/test-id")
        assert response.status_code == 404, response.status_code

    def test_returns_markdown_for_existing(self, authenticated_client):
        """Should return markdown for existing research."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_research = MagicMock()
            mock_research.id = "test-id"
            # report_assembly_service.assemble_full_report joins string
            # parts; supply real types matching production.
            mock_research.report_content = (
                "# Test Research\n\nThis is the markdown content."
            )
            mock_research.research_meta = {}

            mock_query = MagicMock()
            mock_query.filter_by.return_value.first.return_value = mock_research
            mock_query.filter_by.return_value.order_by.return_value.all.return_value = []
            mock_session.query.return_value = mock_query

            with patch(
                "local_deep_research.web.auth.decorators.current_user"
            ) as mock_current_user:
                mock_current_user.return_value = "testuser"

                with patch(
                    "local_deep_research.storage.get_report_storage"
                ) as mock_storage_factory:
                    mock_storage = MagicMock()
                    mock_storage.get_report.return_value = (
                        "# Test Research\n\nThis is the markdown content."
                    )
                    mock_storage_factory.return_value = mock_storage

                    response = authenticated_client.get(
                        f"{HISTORY_PREFIX}/markdown/test-id"
                    )

                    assert response.status_code == 200
                    data = response.get_json()
                    assert data["status"] == "success"
                    assert "content" in data


class TestGetResearchLogs:
    """Tests for /history/logs/<research_id> endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.get(f"{HISTORY_PREFIX}/logs/test-id")
        assert response.status_code == 404, response.status_code

    def test_returns_404_for_nonexistent(self, authenticated_client):
        """Should return 404 for non-existent research."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_query = MagicMock()
            mock_query.filter_by.return_value.first.return_value = None
            mock_session.query.return_value = mock_query

            response = authenticated_client.get(
                f"{HISTORY_PREFIX}/logs/nonexistent-id"
            )

            assert response.status_code == 404

    def test_returns_logs_for_existing(self, authenticated_client):
        """Should return logs for existing research."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_research = MagicMock()
            mock_research.id = "test-id"

            mock_query = MagicMock()
            mock_query.filter_by.return_value.first.return_value = mock_research
            mock_session.query.return_value = mock_query

            with patch(
                "local_deep_research.web.routes.history_routes.get_logs_for_research"
            ) as mock_logs:
                mock_logs.return_value = [
                    {"time": "10:00:00", "message": "Started", "type": "info"},
                    {
                        "time": "10:01:00",
                        "message": "Processing",
                        "type": "info",
                    },
                ]

                response = authenticated_client.get(
                    f"{HISTORY_PREFIX}/logs/test-id"
                )

                assert response.status_code == 200
                data = response.get_json()
                assert data["status"] == "success"
                assert len(data["logs"]) == 2

    def _patch_existing_research(self):
        """Return a context manager that mocks the existence check + returns
        a session whose query.filter_by().first() yields a research row."""
        from contextlib import contextmanager

        @contextmanager
        def _ctx():
            with patch(
                "local_deep_research.web.routes.history_routes.get_user_db_session"
            ) as mock_session_ctx:
                mock_session = MagicMock()
                mock_session_ctx.return_value.__enter__ = MagicMock(
                    return_value=mock_session
                )
                mock_session_ctx.return_value.__exit__ = MagicMock(
                    return_value=None
                )
                mock_research = MagicMock()
                mock_research.id = "test-id"
                mock_query = MagicMock()
                mock_query.filter_by.return_value.first.return_value = (
                    mock_research
                )
                mock_session.query.return_value = mock_query
                yield

        return _ctx()

    def test_default_limit_is_500(self, authenticated_client):
        """When no ?limit= is passed, the route should request 500 rows."""
        with self._patch_existing_research():
            with patch(
                "local_deep_research.web.routes.history_routes.get_logs_for_research"
            ) as mock_logs:
                mock_logs.return_value = []
                response = authenticated_client.get(
                    f"{HISTORY_PREFIX}/logs/test-id"
                )
                assert response.status_code == 200
                # Route MUST pass an explicit limit so the DB can't dump
                # every row into the session identity map.
                assert mock_logs.call_args.kwargs.get("limit") == 500

    def test_explicit_limit_is_forwarded(self, authenticated_client):
        """An explicit ?limit=N below the cap is forwarded verbatim."""
        with self._patch_existing_research():
            with patch(
                "local_deep_research.web.routes.history_routes.get_logs_for_research"
            ) as mock_logs:
                mock_logs.return_value = []
                response = authenticated_client.get(
                    f"{HISTORY_PREFIX}/logs/test-id?limit=42"
                )
                assert response.status_code == 200
                assert mock_logs.call_args.kwargs.get("limit") == 42

    def test_limit_is_clamped_to_5000(self, authenticated_client):
        """A ?limit= above the server-side ceiling is clamped to 5000."""
        with self._patch_existing_research():
            with patch(
                "local_deep_research.web.routes.history_routes.get_logs_for_research"
            ) as mock_logs:
                mock_logs.return_value = []
                response = authenticated_client.get(
                    f"{HISTORY_PREFIX}/logs/test-id?limit=99999"
                )
                assert response.status_code == 200
                assert mock_logs.call_args.kwargs.get("limit") == 5000

    def test_limit_floor_is_1(self, authenticated_client):
        """A ?limit= of 0 or negative is floored to 1."""
        with self._patch_existing_research():
            with patch(
                "local_deep_research.web.routes.history_routes.get_logs_for_research"
            ) as mock_logs:
                mock_logs.return_value = []
                response = authenticated_client.get(
                    f"{HISTORY_PREFIX}/logs/test-id?limit=0"
                )
                assert response.status_code == 200
                assert mock_logs.call_args.kwargs.get("limit") == 1


class TestGetLogCount:
    """Tests for /history/log_count/<research_id> endpoint."""

    def test_requires_authentication(self, client):
        """Should require authentication."""
        response = client.get(f"{HISTORY_PREFIX}/log_count/test-id")
        assert response.status_code == 404, response.status_code

    def test_returns_log_count(self, authenticated_client):
        """Should return log count for research."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_total_logs_for_research"
        ) as mock_total:
            mock_total.return_value = 15

            response = authenticated_client.get(
                f"{HISTORY_PREFIX}/log_count/test-id"
            )

            assert response.status_code == 200
            data = response.get_json()
            assert data["status"] == "success"
            assert data["total_logs"] == 15


class TestGetHistoryMetadataParsing:
    """Tests for JSON metadata parsing in get_history (PR #2013).

    PR #2013 changed bare `except:` to `except json.JSONDecodeError:` to only
    catch JSON parsing errors when deserializing research_meta.
    """

    def test_json_decode_error_caught_specifically(self):
        """Verify json.JSONDecodeError is caught, not a bare except.

        The code calls json.loads on the serialized research_meta. If for any
        reason the stored JSON is invalid, json.JSONDecodeError should be
        caught and metadata should default to {}.
        """
        import json

        # Simulate the exact code pattern from history_routes.py
        item = {"research_meta": "not valid json {{"}

        try:
            metadata = json.loads(item["research_meta"])
            item["metadata"] = metadata
        except json.JSONDecodeError:
            item["metadata"] = {}

        assert item["metadata"] == {}

    def test_json_decode_error_does_not_catch_other_exceptions(self):
        """Verify that non-JSONDecodeError exceptions propagate.

        This confirms the PR #2013 change from bare except to specific
        json.JSONDecodeError - other exceptions should NOT be caught.
        """
        import json

        item = {"research_meta": None}  # json.loads(None) raises TypeError

        with pytest.raises(TypeError):
            try:
                metadata = json.loads(item["research_meta"])
                item["metadata"] = metadata
            except json.JSONDecodeError:
                item["metadata"] = {}

    def test_handles_valid_dict_in_research_meta(self, authenticated_client):
        """Should parse valid dict in research_meta correctly."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_research = MagicMock()
            mock_research.id = "test-id-valid"
            mock_research.title = "Test Research"
            mock_research.query = "Test query"
            mock_research.mode = "quick"
            mock_research.status = "completed"
            mock_research.created_at = "2024-01-01T10:00:00"
            mock_research.completed_at = "2024-01-01T10:05:00"
            mock_research.duration_seconds = 300
            mock_research.report_path = "/path/to/report.md"
            mock_research.research_meta = {"strategy": "evidence_based"}
            mock_research.chat_session_id = None
            mock_research.document_count = 0

            mock_query = MagicMock()
            mock_query.outerjoin.return_value.group_by.return_value.order_by.return_value.limit.return_value.offset.return_value.all.return_value = [
                mock_research
            ]
            mock_session.query.return_value = mock_query

            response = authenticated_client.get(f"{HISTORY_PREFIX}/api")

            assert response.status_code == 200
            data = response.get_json()
            assert data["status"] == "success"
            assert data["items"][0]["metadata"] == {"is_news_search": False}

    def test_handles_none_research_meta(self, authenticated_client):
        """Should use empty dict when research_meta is None/falsy."""
        with patch(
            "local_deep_research.web.routes.history_routes.get_user_db_session"
        ) as mock_session_ctx:
            mock_session = MagicMock()
            mock_session_ctx.return_value.__enter__ = MagicMock(
                return_value=mock_session
            )
            mock_session_ctx.return_value.__exit__ = MagicMock(
                return_value=None
            )

            mock_research = MagicMock()
            mock_research.id = "test-id-none"
            mock_research.title = "Test Research"
            mock_research.query = "Test query"
            mock_research.mode = "quick"
            mock_research.status = "completed"
            mock_research.created_at = "2024-01-01T10:00:00"
            mock_research.completed_at = "2024-01-01T10:05:00"
            mock_research.duration_seconds = 300
            mock_research.report_path = "/path/to/report.md"
            mock_research.research_meta = None
            mock_research.chat_session_id = None
            mock_research.document_count = 0

            mock_query = MagicMock()
            mock_query.outerjoin.return_value.group_by.return_value.order_by.return_value.limit.return_value.offset.return_value.all.return_value = [
                mock_research
            ]
            mock_session.query.return_value = mock_query

            response = authenticated_client.get(f"{HISTORY_PREFIX}/api")

            assert response.status_code == 200
            data = response.get_json()
            assert data["status"] == "success"
            # When research_meta is None, metadata defaults to {is_news_search: False}
            assert data["items"][0]["metadata"] == {"is_news_search": False}
