"""Regression test: persistence-engine sqlite dir setup must run off the loop.

Anchors the production offload in `persistence/engine.py:init_engine`, where the
SQLite data directory is created with `os.makedirs`. `init_engine` runs on the
FastAPI lifespan event loop, so a sync `os.makedirs` (a stat + mkdir syscall)
there blocks startup — the same class of bug fixed for the checkpointer's
`ensure_sqlite_parent_dir` in #1912 (see `test_sqlite_lifespan.py`).

This invokes the production `init_engine(backend="sqlite", ...)` under the strict
Blockbuster context with a `sqlite_dir` that does not yet exist, so `os.makedirs`
actually runs. The async engine/session machinery is mocked out so the only host
filesystem operation under test is the directory creation; if it regresses to run
directly on the event loop, Blockbuster raises `BlockingError` and this fails.

We also stub ``bootstrap_schema`` so the alembic stamp/upgrade path -- which has
its own ``asyncio.to_thread`` regression anchor in
``test_persistence_bootstrap.py`` -- does not turn this test into a
double-coverage one. Keeping concerns separated means a regression in either
offload (makedirs vs alembic) points at the right place.
"""

from __future__ import annotations

from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

# Pre-import so `init_engine`'s lazy ``import deerflow.persistence.models`` is a
# cached no-op rather than a file read under the strict gate.
import deerflow.persistence.models  # noqa: E402,F401
from deerflow.persistence import engine as engine_mod  # noqa: E402

pytestmark = pytest.mark.asyncio


def _noop_listens_for(*_args, **_kwargs):
    """Decorator factory that registers nothing (mock engine has no real events)."""

    def _decorator(fn):
        return fn

    return _decorator


async def test_init_engine_sqlite_dir_setup_does_not_block_event_loop(tmp_path: Path) -> None:
    data_dir = tmp_path / "newsubdir"  # does not exist yet -> os.makedirs runs
    db_file = data_dir / "app.db"

    mock_conn = AsyncMock()
    begin_ctx = AsyncMock()
    begin_ctx.__aenter__.return_value = mock_conn
    begin_ctx.__aexit__.return_value = False
    mock_engine = MagicMock()
    mock_engine.begin.return_value = begin_ctx
    mock_engine.dispose = AsyncMock()

    async def _noop_bootstrap(*_args, **_kwargs):
        return None

    with (
        patch.object(engine_mod, "create_async_engine", return_value=mock_engine),
        patch.object(engine_mod, "async_sessionmaker", return_value=MagicMock()),
        patch("sqlalchemy.event.listens_for", _noop_listens_for),
        patch(
            "deerflow.persistence.bootstrap.bootstrap_schema",
            new=_noop_bootstrap,
        ),
    ):
        await engine_mod.init_engine(
            backend="sqlite",
            url=f"sqlite+aiosqlite:///{db_file}",
            sqlite_dir=str(data_dir),
        )
        assert data_dir.exists()

    await engine_mod.close_engine()
