"""
Tests for cleanup_db_session teardown behavior in apply_middleware().

Source: app_factory.py lines 547-596.
Tests the teardown_appcontext handler that cleans up DB sessions,
thread-local sessions, and sweeps dead engines after each request.
"""

import pytest
from unittest.mock import patch, Mock

from flask import g


@pytest.fixture
def app():
    """Create a minimal Flask app with the teardown handler registered."""
    from local_deep_research.web.app_factory import create_app

    with patch("local_deep_research.web.app_factory.SocketIOService"):
        app, _ = create_app()
        app.config["TESTING"] = True
        app.config["WTF_CSRF_ENABLED"] = False
        return app


class TestCleanupDbSession:
    """Tests for the cleanup_db_session teardown handler."""

    def test_normal_cleanup_rollback_and_close(self, app):
        """Session in g.db_session gets rollback() and close() called."""
        mock_session = Mock()

        with app.test_request_context("/"):
            g.db_session = mock_session

        # After exiting context, teardown runs
        mock_session.rollback.assert_called_once()
        mock_session.close.assert_called_once()

    def test_no_session_in_g_no_error(self, app):
        """When g has no db_session, teardown completes without error."""
        with app.test_request_context("/"):
            # Don't set g.db_session
            pass
        # Should not raise

    def test_rollback_fails_close_still_called(self, app):
        """If rollback() raises, close() is still called."""
        mock_session = Mock()
        mock_session.rollback.side_effect = RuntimeError("rollback failed")

        with app.test_request_context("/"):
            g.db_session = mock_session

        mock_session.rollback.assert_called_once()
        mock_session.close.assert_called_once()

    def test_close_fails_no_propagation(self, app):
        """If close() raises, the exception does not propagate."""
        mock_session = Mock()
        mock_session.close.side_effect = RuntimeError("close failed")

        with app.test_request_context("/"):
            g.db_session = mock_session

        # Should not raise despite close() failure
        mock_session.rollback.assert_called_once()
        mock_session.close.assert_called_once()

    def test_cleanup_current_thread_called(self, app):
        """cleanup_current_thread from thread_local_session is called."""
        mock_cleanup = Mock()

        with patch(
            "local_deep_research.database.thread_local_session.cleanup_current_thread",
            mock_cleanup,
        ):
            with app.test_request_context("/"):
                pass

        mock_cleanup.assert_called_once()

    def test_cleanup_current_thread_import_fails_no_error(self, app):
        """If cleanup_current_thread import fails, no exception propagates."""
        with patch(
            "local_deep_research.database.thread_local_session.cleanup_current_thread",
            side_effect=ImportError("no module"),
        ):
            with app.test_request_context("/"):
                pass
        # Should not raise

    def test_maybe_sweep_dead_engines_called(self, app):
        """db_manager.maybe_sweep_dead_engines is called during teardown."""
        mock_db_manager = Mock()

        with patch(
            "local_deep_research.database.encrypted_db.db_manager",
            mock_db_manager,
        ):
            with app.test_request_context("/"):
                pass

        mock_db_manager.maybe_sweep_dead_engines.assert_called_once()

    def test_thread_session_manager_cleanup_called(self, app):
        """thread_session_manager.cleanup_dead_threads is called during teardown."""
        mock_manager = Mock()

        with patch(
            "local_deep_research.database.thread_local_session.thread_session_manager",
            mock_manager,
        ):
            with app.test_request_context("/"):
                pass

        mock_manager.cleanup_dead_threads.assert_called_once()
