"""
Coverage tests for local_deep_research/web/app.py.

Targets the ~50 missing statements in the main() function by patching
heavy dependencies and exercising each branch individually.

All lazy imports inside main() (start_connection_cleanup_scheduler,
session_manager, db_manager) are patched at their canonical module paths
because they are not present as attributes on the app module itself.
"""

import os
from unittest.mock import MagicMock, patch

import pytest


MODULE = "local_deep_research.web.app"

# Canonical patch targets for lazy imports inside main()
_CLEANUP_MOD = "local_deep_research.web.auth.connection_cleanup"
_SESSION_MOD = "local_deep_research.web.auth.session_manager"
_DB_MOD = "local_deep_research.database.encrypted_db"


@pytest.fixture(autouse=True)
def _stub_log_queue_daemon():
    """Prevent main() from spawning a real log-queue daemon thread during tests."""
    with (
        patch(f"{MODULE}.start_log_queue_processor"),
        patch(f"{MODULE}.stop_log_queue_processor"),
    ):
        yield


def _handler_by_name(captured, name):
    """Locate an atexit handler by function name (order-independent)."""
    for fn in captured:
        if getattr(fn, "__name__", None) == name:
            return fn
    raise AssertionError(
        f"No captured handler named {name!r}; got "
        f"{[getattr(fn, '__name__', repr(fn)) for fn in captured]}"
    )


# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------


def _make_config(
    host="127.0.0.1",
    port=5000,
    debug=False,
    use_https=False,
):
    return {
        "host": host,
        "port": port,
        "debug": debug,
        "use_https": use_https,
    }


def _patch_main(config, env=None, cleanup_side_effect=None):
    """Return a context-manager stack that patches everything main() needs.

    Usage::

        with _patch_main(config) as mocks:
            from local_deep_research.web import app as app_module
            app_module.main()
        mocks["socket_service"].run.assert_called_once()
    """
    mock_socket_service = MagicMock()
    mock_app = MagicMock()
    mock_scheduler = MagicMock()

    patches = [
        patch(f"{MODULE}.load_server_config", return_value=config),
        patch(f"{MODULE}.config_logger"),
        patch(
            f"{MODULE}.create_app", return_value=(mock_app, mock_socket_service)
        ),
        patch("atexit.register"),
        patch(
            f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
            side_effect=cleanup_side_effect,
            return_value=mock_scheduler,
        ),
        patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
        patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
    ]

    env_patch = patch.dict("os.environ", env or {}, clear=False)

    class _Stack:
        def __enter__(self_inner):
            env_patch.start()
            for p in patches:
                p.start()
            self_inner.mocks = {
                "socket_service": mock_socket_service,
                "app": mock_app,
                "scheduler": mock_scheduler,
            }
            return self_inner.mocks

        def __exit__(self_inner, *_):
            for p in reversed(patches):
                p.stop()
            env_patch.stop()

    return _Stack()


# ---------------------------------------------------------------------------
# Tests: logging configuration
# ---------------------------------------------------------------------------


class TestMainLogging:
    """main() configures logging with the correct debug flag."""

    def test_config_logger_called_with_debug_true(self):
        config = _make_config(debug=True)
        with _patch_main(config, env={"WERKZEUG_RUN_MAIN": "true"}):
            with patch(f"{MODULE}.config_logger") as mock_cl:
                from local_deep_research.web import app as app_module

                app_module.main()
        mock_cl.assert_called_once_with("ldr_web", debug=True)

    def test_config_logger_called_with_debug_false(self):
        config = _make_config(debug=False)
        with _patch_main(config):
            with patch(f"{MODULE}.config_logger") as mock_cl:
                from local_deep_research.web import app as app_module

                app_module.main()
        mock_cl.assert_called_once_with("ldr_web", debug=False)


# ---------------------------------------------------------------------------
# Tests: HTTPS branch
# ---------------------------------------------------------------------------


class TestMainHttpsBranch:
    """main() logs a warning when use_https=True."""

    def test_https_branch_does_not_raise(self):
        """use_https=True must not raise; it only emits log warnings."""
        # audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
        config = _make_config(use_https=True)
        with _patch_main(config):
            from local_deep_research.web import app as app_module

            app_module.main()  # must complete without exception

    def test_no_https_branch_runs_cleanly(self):
        """use_https=False takes the happy path without any HTTPS logging."""
        # audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
        config = _make_config(use_https=False)
        with _patch_main(config):
            from local_deep_research.web import app as app_module

            app_module.main()


# ---------------------------------------------------------------------------
# Tests: cleanup-scheduler startup branch
# ---------------------------------------------------------------------------


class TestMainCleanupSchedulerBranch:
    """Guards for the conditional scheduler startup inside main()."""

    def test_scheduler_started_when_not_debug(self):
        """debug=False always starts the cleanup scheduler."""
        config = _make_config(debug=False)
        with (
            patch(f"{MODULE}.load_server_config", return_value=config),
            patch(f"{MODULE}.config_logger"),
            patch(
                f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
            ),
            patch("atexit.register"),
            patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
            patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
            patch(
                f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
                return_value=MagicMock(),
            ) as mock_start,
        ):
            from local_deep_research.web import app as app_module

            app_module.main()
        mock_start.assert_called_once()

    def test_scheduler_started_when_werkzeug_run_main_set(self):
        """debug=True with WERKZEUG_RUN_MAIN=true still starts the scheduler."""
        config = _make_config(debug=True)
        with (
            patch(f"{MODULE}.load_server_config", return_value=config),
            patch(f"{MODULE}.config_logger"),
            patch(
                f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
            ),
            patch("atexit.register"),
            patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
            patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
            patch(
                f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
                return_value=MagicMock(),
            ) as mock_start,
            patch.dict(
                "os.environ", {"WERKZEUG_RUN_MAIN": "true"}, clear=False
            ),
        ):
            from local_deep_research.web import app as app_module

            app_module.main()
        mock_start.assert_called_once()

    def test_scheduler_not_started_in_debug_without_werkzeug(self):
        """debug=True without WERKZEUG_RUN_MAIN skips scheduler startup."""
        config = _make_config(debug=True)
        # Ensure WERKZEUG_RUN_MAIN is absent for this test
        env_without = {
            k: v for k, v in os.environ.items() if k != "WERKZEUG_RUN_MAIN"
        }
        with (
            patch(f"{MODULE}.load_server_config", return_value=config),
            patch(f"{MODULE}.config_logger"),
            patch(
                f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
            ),
            patch("atexit.register"),
            patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
            patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
            patch(
                f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
                return_value=MagicMock(),
            ) as mock_start,
            patch.dict("os.environ", env_without, clear=True),
        ):
            from local_deep_research.web import app as app_module

            app_module.main()
        mock_start.assert_not_called()

    def test_scheduler_failure_does_not_abort_startup(self):
        """If start_connection_cleanup_scheduler raises, main() still runs the server."""
        config = _make_config(debug=False)
        mock_socket_service = MagicMock()
        with (
            patch(f"{MODULE}.load_server_config", return_value=config),
            patch(f"{MODULE}.config_logger"),
            patch(
                f"{MODULE}.create_app",
                return_value=(MagicMock(), mock_socket_service),
            ),
            patch("atexit.register"),
            patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
            patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
            patch(
                f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
                side_effect=RuntimeError("scheduler boom"),
            ),
        ):
            from local_deep_research.web import app as app_module

            app_module.main()  # must not raise
        mock_socket_service.run.assert_called_once()


# ---------------------------------------------------------------------------
# Tests: atexit registrations
# ---------------------------------------------------------------------------


class TestMainAtexitRegistrations:
    """main() must register at least 3 atexit handlers."""

    def test_multiple_atexit_handlers_registered(self):
        config = _make_config()
        with (
            patch(f"{MODULE}.load_server_config", return_value=config),
            patch(f"{MODULE}.config_logger"),
            patch(
                f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
            ),
            patch("atexit.register") as mock_reg,
            patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
            patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
            patch(
                f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
                return_value=MagicMock(),
            ),
        ):
            from local_deep_research.web import app as app_module

            app_module.main()
        # Expect: shutdown_databases, shutdown_scheduler, cleanup-scheduler
        # lambda, flush_logs_on_exit, stop_log_queue_processor
        assert mock_reg.call_count >= 3


# ---------------------------------------------------------------------------
# Tests: socket_service.run() call
# ---------------------------------------------------------------------------


class TestMainSocketServiceRun:
    """main() forwards host/port/debug to socket_service.run()."""

    def test_run_called_with_host_port_debug(self):
        config = _make_config(host="0.0.0.0", port=8080, debug=False)
        mock_socket_service = MagicMock()
        with (
            patch(f"{MODULE}.load_server_config", return_value=config),
            patch(f"{MODULE}.config_logger"),
            patch(
                f"{MODULE}.create_app",
                return_value=(MagicMock(), mock_socket_service),
            ),
            patch("atexit.register"),
            patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
            patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
            patch(
                f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
                return_value=MagicMock(),
            ),
        ):
            from local_deep_research.web import app as app_module

            app_module.main()
        mock_socket_service.run.assert_called_once_with(
            host="0.0.0.0", port=8080, debug=False
        )

    def test_run_called_with_debug_true(self):
        config = _make_config(host="localhost", port=5001, debug=True)
        mock_socket_service = MagicMock()
        with (
            patch(f"{MODULE}.load_server_config", return_value=config),
            patch(f"{MODULE}.config_logger"),
            patch(
                f"{MODULE}.create_app",
                return_value=(MagicMock(), mock_socket_service),
            ),
            patch("atexit.register"),
            patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
            patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
            patch(
                f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
                return_value=MagicMock(),
            ),
            patch.dict(
                "os.environ", {"WERKZEUG_RUN_MAIN": "true"}, clear=False
            ),
        ):
            from local_deep_research.web import app as app_module

            app_module.main()
        mock_socket_service.run.assert_called_once_with(
            host="localhost", port=5001, debug=True
        )


# ---------------------------------------------------------------------------
# Tests: flush_logs_on_exit closure
# ---------------------------------------------------------------------------


class TestFlushLogsOnExit:
    """The first atexit handler (flush_logs_on_exit) must flush the log queue."""

    def _run_and_capture(self, extra_patches=None):
        captured = []
        config = _make_config()
        extra_patches = extra_patches or []

        patches = [
            patch(f"{MODULE}.load_server_config", return_value=config),
            patch(f"{MODULE}.config_logger"),
            patch(
                f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
            ),
            patch("atexit.register", side_effect=captured.append),
            patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
            patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
            patch(
                f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
                return_value=MagicMock(),
            ),
        ] + extra_patches

        for p in patches:
            p.start()
        try:
            from local_deep_research.web import app as app_module

            app_module.main()
        finally:
            for p in reversed(patches):
                p.stop()
        return captured

    def test_flush_handler_calls_flush_log_queue(self):
        with patch(f"{MODULE}.flush_log_queue") as mock_flush:
            captured = self._run_and_capture()
            _handler_by_name(captured, "flush_logs_on_exit")()
            mock_flush.assert_called_once()

    def test_flush_handler_swallows_exceptions(self):
        # audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
        with patch(
            f"{MODULE}.flush_log_queue", side_effect=RuntimeError("log error")
        ):
            captured = self._run_and_capture()
        _handler_by_name(captured, "flush_logs_on_exit")()  # must not raise


# ---------------------------------------------------------------------------
# Tests: shutdown_scheduler closure
# ---------------------------------------------------------------------------


class TestShutdownSchedulerHandler:
    """The shutdown_scheduler atexit handler."""

    def _run_and_capture_handlers(self, mock_app):
        captured = []
        config = _make_config()

        patches = [
            patch(f"{MODULE}.load_server_config", return_value=config),
            patch(f"{MODULE}.config_logger"),
            patch(f"{MODULE}.create_app", return_value=(mock_app, MagicMock())),
            patch("atexit.register", side_effect=captured.append),
            patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
            patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
            patch(
                f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
                return_value=MagicMock(),
            ),
        ]
        for p in patches:
            p.start()
        try:
            from local_deep_research.web import app as app_module

            app_module.main()
        finally:
            for p in reversed(patches):
                p.stop()
        return captured

    def test_shutdown_scheduler_stops_news_scheduler(self):
        mock_app = MagicMock()
        mock_background_job_scheduler = MagicMock()
        mock_app.background_job_scheduler = mock_background_job_scheduler

        captured = self._run_and_capture_handlers(mock_app)
        for handler in captured:
            try:
                handler()
            except Exception:
                pass
        mock_background_job_scheduler.stop.assert_called_once()

    def test_shutdown_scheduler_noop_without_news_scheduler(self):
        """app with no news_scheduler attribute does not raise."""
        # audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
        mock_app = MagicMock(spec=[])  # no attributes at all

        captured = self._run_and_capture_handlers(mock_app)
        for handler in captured:
            try:
                handler()
            except Exception:
                pass  # other handlers (e.g. shutdown_databases) may raise; that is fine


# ---------------------------------------------------------------------------
# Tests: shutdown_databases closure
# ---------------------------------------------------------------------------


class TestShutdownDatabasesHandler:
    """The shutdown_databases atexit handler (last registered)."""

    def _run_and_capture_handlers(self):
        captured = []
        config = _make_config()

        patches = [
            patch(f"{MODULE}.load_server_config", return_value=config),
            patch(f"{MODULE}.config_logger"),
            patch(
                f"{MODULE}.create_app", return_value=(MagicMock(), MagicMock())
            ),
            patch("atexit.register", side_effect=captured.append),
            patch(f"{_SESSION_MOD}.session_manager", MagicMock(), create=True),
            patch(f"{_DB_MOD}.db_manager", MagicMock(), create=True),
            patch(
                f"{_CLEANUP_MOD}.start_connection_cleanup_scheduler",
                return_value=MagicMock(),
            ),
        ]
        for p in patches:
            p.start()
        try:
            from local_deep_research.web import app as app_module

            app_module.main()
        finally:
            for p in reversed(patches):
                p.stop()
        return captured

    def test_shutdown_databases_calls_close_all_databases(self):
        mock_db_manager = MagicMock()
        captured = self._run_and_capture_handlers()
        handler = _handler_by_name(captured, "shutdown_databases")
        with patch(f"{_DB_MOD}.db_manager", mock_db_manager):
            handler()
        mock_db_manager.close_all_databases.assert_called_once()

    def test_shutdown_databases_swallows_exceptions(self):
        # audit: PUNCHLIST reviewed 2026-05 — KEEP (ASSERT_TRUE).
        captured = self._run_and_capture_handlers()
        handler = _handler_by_name(captured, "shutdown_databases")
        with patch(f"{_DB_MOD}.db_manager", side_effect=ImportError("db gone")):
            handler()  # must not propagate
