"""Tests for ``mgf.alembic.configure_env``.

Strategy: build a minimal alembic project in a tmp dir + run
``alembic.command.upgrade(config, "head")`` via the public alembic
API (no subprocess). The env.py imports configure_env, which is the
load-bearing path under test.

Two driver shapes covered:

  - Sync (``sqlite:///``): smoke + round-trip.
  - Async (``sqlite+aiosqlite://``): smoke + round-trip.
"""

from __future__ import annotations

import textwrap
from typing import TYPE_CHECKING

import pytest
from alembic import command
from alembic.config import Config
from mgf.common.exceptions import OperationError
from sqlalchemy import create_engine, text

if TYPE_CHECKING:
    from pathlib import Path


# ---------------------------------------------------------------------------
# Test alembic project scaffolding.
# ---------------------------------------------------------------------------


_ENV_PY_TEMPLATE = textwrap.dedent(
    """
    from mgf.alembic import configure_env
    from sqlalchemy import Column, Integer, MetaData, String, Table

    metadata = MetaData()
    Table(
        "users",
        metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(64), nullable=False),
    )

    configure_env(
        database_url={url!r},
        target_metadata=metadata,
    )
    """
).strip()


_INITIAL_MIGRATION_TEMPLATE = textwrap.dedent(
    '''
    """initial — create users table

    Revision ID: 0001
    Revises:
    Create Date: 2026-04-29 00:00:00

    """
    from alembic import op
    import sqlalchemy as sa

    revision = "0001"
    down_revision = None
    branch_labels = None
    depends_on = None


    def upgrade() -> None:
        op.create_table(
            "users",
            sa.Column("id", sa.Integer(), primary_key=True),
            sa.Column("name", sa.String(64), nullable=False),
        )


    def downgrade() -> None:
        op.drop_table("users")
    '''
).strip()


_ALEMBIC_INI_TEMPLATE = textwrap.dedent(
    """
    [alembic]
    script_location = {script_location}
    sqlalchemy.url = {url}

    [loggers]
    keys = root

    [handlers]
    keys = console

    [formatters]
    keys = generic

    [logger_root]
    level = WARN
    handlers = console
    qualname =

    [handler_console]
    class = StreamHandler
    args = (sys.stderr,)
    level = NOTSET
    formatter = generic

    [formatter_generic]
    format = %(levelname)-5.5s [%(name)s] %(message)s
    """
).strip()


def _build_project(tmp_path: Path, *, url: str) -> Config:
    """Lay out a minimal alembic project in ``tmp_path``; return Config."""
    project_root = tmp_path / "alembic_test"
    project_root.mkdir()

    versions = project_root / "versions"
    versions.mkdir()

    (project_root / "env.py").write_text(_ENV_PY_TEMPLATE.format(url=url))
    (project_root / "script.py.mako").write_text(
        '"""${message}"""\n'
        "from alembic import op\n"
        "import sqlalchemy as sa\n"
        "revision = ${repr(up_revision)}\n"
        "down_revision = ${repr(down_revision)}\n"
        "branch_labels = ${repr(branch_labels)}\n"
        "depends_on = ${repr(depends_on)}\n"
        "\n"
        "def upgrade() -> None:\n"
        '    ${upgrades if upgrades else "pass"}\n'
        "\n"
        "def downgrade() -> None:\n"
        '    ${downgrades if downgrades else "pass"}\n'
    )
    (versions / "0001_initial.py").write_text(_INITIAL_MIGRATION_TEMPLATE)

    ini_path = tmp_path / "alembic.ini"
    ini_path.write_text(
        _ALEMBIC_INI_TEMPLATE.format(
            script_location=str(project_root),
            url=url,
        )
    )

    return Config(str(ini_path))


# ---------------------------------------------------------------------------
# Sync driver round-trip.
# ---------------------------------------------------------------------------


class TestSyncDriver:
    def test_upgrade_creates_users_table(self, tmp_path: Path) -> None:
        db_path = tmp_path / "test.db"
        url = f"sqlite:///{db_path}"
        config = _build_project(tmp_path, url=url)

        # Run the migration.
        command.upgrade(config, "head")

        # Verify the table exists.
        engine = create_engine(url)
        try:
            with engine.connect() as conn:
                result = conn.execute(text("SELECT name FROM sqlite_master WHERE type='table'"))
                tables = {row[0] for row in result}
        finally:
            engine.dispose()

        assert "users" in tables
        assert "alembic_version" in tables  # Tracked migrations.

    def test_downgrade_drops_users_table(self, tmp_path: Path) -> None:
        db_path = tmp_path / "test.db"
        url = f"sqlite:///{db_path}"
        config = _build_project(tmp_path, url=url)

        command.upgrade(config, "head")
        command.downgrade(config, "base")

        engine = create_engine(url)
        try:
            with engine.connect() as conn:
                result = conn.execute(text("SELECT name FROM sqlite_master WHERE type='table'"))
                tables = {row[0] for row in result}
        finally:
            engine.dispose()

        # users table is gone; alembic_version stays.
        assert "users" not in tables


# ---------------------------------------------------------------------------
# Async driver round-trip.
# ---------------------------------------------------------------------------


# aiosqlite's worker thread + the asyncio event loop our helper
# spins up live past the asyncio.run() return; finalizers fire during
# the next test's GC pass and surface as PytestUnraisableExceptionWarning.
# Force a GC sweep at end-of-test (within this filter scope) so the
# finalizer warnings are absorbed here rather than leaking to siblings.
@pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
class TestAsyncDriver:
    def test_async_upgrade_creates_users_table(self, tmp_path: Path) -> None:
        import gc

        db_path = tmp_path / "test.db"
        url = f"sqlite+aiosqlite:///{db_path}"
        config = _build_project(tmp_path, url=url)

        # Run the migration (configure_env detects the async URL +
        # uses asyncio.run internally).
        command.upgrade(config, "head")

        # Verify via a sync sqlite reader (the file is the same).
        engine = create_engine(f"sqlite:///{db_path}")
        try:
            with engine.connect() as conn:
                result = conn.execute(text("SELECT name FROM sqlite_master WHERE type='table'"))
                tables = {row[0] for row in result}
        finally:
            engine.dispose()

        assert "users" in tables

        # Sweep aiosqlite/loop finalizers within this test's filter
        # scope so they don't surface in unrelated downstream tests.
        gc.collect()


# ---------------------------------------------------------------------------
# Offline mode (SQL-emit-only).
# ---------------------------------------------------------------------------


class TestOfflineMode:
    def test_offline_emits_sql(self, tmp_path: Path, capsys) -> None:
        url = "sqlite:///./not-used.db"  # offline doesn't connect
        config = _build_project(tmp_path, url=url)

        # alembic.command.upgrade with sql=True is offline mode.
        command.upgrade(config, "head", sql=True)

        captured = capsys.readouterr()
        # The output should contain the CREATE TABLE statement.
        assert "CREATE TABLE users" in captured.out
        assert "alembic_version" in captured.out

    def test_offline_mode_emits_stderr_warning(self, tmp_path: Path, capsys) -> None:
        """SH-02: offline mode prints a stderr warning so operators
        piping `alembic upgrade --sql` see the credential-leak caveat
        before the SQL output lands in a file or scrollback."""
        url = "sqlite:///./not-used.db"
        config = _build_project(tmp_path, url=url)

        command.upgrade(config, "head", sql=True)

        captured = capsys.readouterr()
        # The warning lives on stderr — stdout carries the SQL.
        assert "mgf.alembic" in captured.err
        assert "offline mode" in captured.err
        # Mentions the mitigation hook so an operator can find the
        # tracker entry without grepping logs.
        assert "SH-02" in captured.err

    def test_online_mode_does_not_emit_offline_warning(self, tmp_path: Path, capsys) -> None:
        """SH-02: the warning is offline-mode-specific. A regular
        `alembic upgrade head` (online) does NOT print it — operators
        running real migrations shouldn't see the warning every time."""
        db_path = tmp_path / "test.db"
        url = f"sqlite:///{db_path}"
        config = _build_project(tmp_path, url=url)

        command.upgrade(config, "head")

        captured = capsys.readouterr()
        # The warning's signature substring; absent on online path.
        assert "mgf.alembic: running in offline mode" not in captured.err


# ---------------------------------------------------------------------------
# Naming convention application.
# ---------------------------------------------------------------------------


_ENV_PY_WITH_NAMING = textwrap.dedent(
    """
    from mgf.alembic import configure_env
    from sqlalchemy import Column, Integer, MetaData, String, Table, UniqueConstraint

    metadata = MetaData()
    Table(
        "users",
        metadata,
        Column("id", Integer, primary_key=True),
        Column("email", String(64), nullable=False),
        UniqueConstraint("email"),
    )

    configure_env(
        database_url={url!r},
        target_metadata=metadata,
        naming_convention={{
            "ix": "ix_%(column_0_label)s",
            "uq": "uq_%(table_name)s_%(column_0_name)s",
            "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
            "pk": "pk_%(table_name)s",
        }},
    )
    """
).strip()


class TestNamingConvention:
    def test_naming_convention_applied(self, tmp_path: Path) -> None:
        # Build a project that uses naming_convention.
        url = f"sqlite:///{tmp_path / 'test.db'}"
        project_root = tmp_path / "alembic_test"
        project_root.mkdir()
        (project_root / "versions").mkdir()
        (project_root / "env.py").write_text(_ENV_PY_WITH_NAMING.format(url=url))
        (project_root / "script.py.mako").write_text(
            '"""${message}"""\n'
            "from alembic import op\n"
            "import sqlalchemy as sa\n"
            "revision = ${repr(up_revision)}\n"
            "down_revision = ${repr(down_revision)}\n"
            "branch_labels = ${repr(branch_labels)}\n"
            "depends_on = ${repr(depends_on)}\n"
            "\n"
            "def upgrade() -> None:\n"
            '    ${upgrades if upgrades else "pass"}\n'
            "\n"
            "def downgrade() -> None:\n"
            '    ${downgrades if downgrades else "pass"}\n'
        )
        ini_path = tmp_path / "alembic.ini"
        ini_path.write_text(
            _ALEMBIC_INI_TEMPLATE.format(
                script_location=str(project_root),
                url=url,
            )
        )

        # Smoke: just verify configure_env runs without error when
        # naming_convention is supplied. Full propagation testing
        # requires an autogenerate run, which is more elaborate.
        config = Config(str(ini_path))
        # No migration files yet — but configure_env still runs.
        command.upgrade(config, "head")  # no-op since versions/ is empty


# ---------------------------------------------------------------------------
# include_object / include_schemas pass-through (PAPER-22, shipped in
# mgf-common v0.19; carried over to mgf-alembic v0.1).
# ---------------------------------------------------------------------------


_ENV_PY_WITH_INCLUDE_OBJECT = textwrap.dedent(
    """
    from mgf.alembic import configure_env
    from sqlalchemy import Column, Integer, MetaData, String, Table

    metadata = MetaData()
    Table(
        "users",
        metadata,
        Column("id", Integer, primary_key=True),
        Column("name", String(64), nullable=False),
    )

    # Marker file the test reads to confirm the filter ran with the
    # right argument shape. Filter excludes the synthetic
    # ``spatial_ref_sys`` object name.
    _CALLS_PATH = {calls_path!r}

    def _filter(obj, name, type_, reflected, compare_to):
        with open(_CALLS_PATH, "a") as fh:
            fh.write(f"{{name}}|{{type_}}\\n")
        return not (type_ == "table" and name == "spatial_ref_sys")

    configure_env(
        database_url={url!r},
        target_metadata=metadata,
        include_object=_filter,
        include_schemas=True,
    )
    """
).strip()


class TestIncludeObjectFilter:
    """PAPER-22 — ``include_object`` + ``include_schemas`` pass-through.

    The callable is forwarded to :func:`alembic.context.configure`;
    autogenerate calls it for every reflected object. We don't run an
    autogenerate cycle here (that requires a live DB), but we verify
    the kwarg lands on ``context.configure`` by inspecting calls
    against a monkeypatched configure spy.
    """

    def test_kwargs_forwarded_to_context_configure_offline(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        from alembic import context as alembic_context

        captured: dict[str, object] = {}

        real_configure = alembic_context.configure

        def _spy_configure(**kwargs: object) -> None:
            captured.update(kwargs)
            return real_configure(**kwargs)

        monkeypatch.setattr(alembic_context, "configure", _spy_configure)

        url = f"sqlite:///{tmp_path / 'test.db'}"
        config = _build_project(tmp_path, url=url)
        # Re-write env.py to use the include_object form.
        calls_path = tmp_path / "filter_calls.txt"
        (tmp_path / "alembic_test" / "env.py").write_text(
            _ENV_PY_WITH_INCLUDE_OBJECT.format(
                url=url,
                calls_path=str(calls_path),
            )
        )

        command.upgrade(config, "head", sql=True)

        assert "include_object" in captured
        assert callable(captured["include_object"])
        assert captured["include_schemas"] is True

    def test_kwargs_default_to_none_and_false_when_not_supplied(
        self,
        tmp_path: Path,
        monkeypatch: pytest.MonkeyPatch,
    ) -> None:
        from alembic import context as alembic_context

        captured: dict[str, object] = {}
        real_configure = alembic_context.configure

        def _spy_configure(**kwargs: object) -> None:
            captured.update(kwargs)
            return real_configure(**kwargs)

        monkeypatch.setattr(alembic_context, "configure", _spy_configure)

        url = f"sqlite:///{tmp_path / 'test.db'}"
        config = _build_project(tmp_path, url=url)
        command.upgrade(config, "head", sql=True)

        # Defaults should be passed through verbatim — alembic treats
        # include_object=None as "include everything".
        assert captured["include_object"] is None
        assert captured["include_schemas"] is False


class TestSafeUrl:
    """SH-01 — `_safe_url` masks the password in DB URLs.

    Used internally by the connect-path wrap so a SQLAlchemy /
    driver exception that quoted the URL verbatim doesn't leak
    credentials to operator log files.
    """

    def test_password_masked(self) -> None:
        from mgf.alembic._env import _safe_url

        url = "postgresql://user:supersecret@host:5432/db"
        safe = _safe_url(url)

        assert "supersecret" not in safe, f"_safe_url did not mask the password: {safe!r}"
        # Username + host + dbname stay — they're not the secret.
        assert "user" in safe
        assert "host" in safe
        assert "db" in safe

    def test_async_driver_url(self) -> None:
        from mgf.alembic._env import _safe_url

        url = "postgresql+asyncpg://app:hunter2@db.example.com:5432/prod"
        safe = _safe_url(url)
        assert "hunter2" not in safe
        assert "asyncpg" in safe

    def test_no_password_url_round_trips(self) -> None:
        from mgf.alembic._env import _safe_url

        url = "sqlite:///./test.db"
        safe = _safe_url(url)
        # Nothing to mask; safe form is equivalent to the input.
        assert "sqlite" in safe

    def test_malformed_url_returns_marker(self) -> None:
        from mgf.alembic._env import _safe_url

        # Wholly malformed input that make_url can't parse.
        safe = _safe_url("not a url at all !! @@")
        # Either returns the sentinel marker or whatever make_url
        # tolerates — the contract is "doesn't raise"; either is
        # fine. The key invariant: callers never see an exception.
        assert isinstance(safe, str)


class TestSafeErrorContext:
    """SH-01 — `_safe_error_context` rewrites exception messages
    to strip the raw URL out of the args tuple."""

    def test_substitutes_url_in_exception_message(self) -> None:
        from mgf.alembic._env import _safe_error_context, _safe_url

        url = "postgresql://user:topsecret@host/db"
        safe = _safe_url(url)

        with pytest.raises(RuntimeError) as exc_info, _safe_error_context(url):
            # Simulate the failure mode: a SQLAlchemy / driver
            # error that quoted the URL with credentials verbatim
            # in its message.
            raise RuntimeError(f"connection failed for {url}")

        msg = str(exc_info.value)
        assert "topsecret" not in msg, (
            f"_safe_error_context left the password in the message: {msg!r}"
        )
        assert safe in msg, f"_safe_error_context did not substitute the safe form: {msg!r}"

    def test_preserves_exception_type(self) -> None:
        from mgf.alembic._env import _safe_error_context

        url = "postgresql://user:topsecret@host/db"

        # Type must propagate unchanged — callers catching specific
        # exception types still match.
        with pytest.raises(ValueError), _safe_error_context(url):
            raise ValueError(f"oops {url}")

    def test_no_op_when_url_absent_from_message(self) -> None:
        from mgf.alembic._env import _safe_error_context

        url = "postgresql://user:secret@host/db"

        # The URL never appears in the message — wrapper should
        # leave the exception args identical.
        with pytest.raises(RuntimeError) as exc_info, _safe_error_context(url):
            raise RuntimeError("generic failure")
        assert str(exc_info.value) == "generic failure"

    def test_does_not_swallow_exception(self) -> None:
        from mgf.alembic._env import _safe_error_context

        url = "postgresql://user:secret@host/db"

        # No exception → wrapper is a no-op.
        result = []
        with _safe_error_context(url):
            result.append("ran")
        assert result == ["ran"]


class TestRunningLoopGuard:
    """RA-01: configure_env's async path raises a typed OperationError
    when called from within a running event loop. Standard alembic
    CLI works fine (no loop active); the guard is for embedded
    callers (pytest-asyncio fixtures, notebooks, embedded test
    harnesses) that would otherwise see the cryptic upstream
    `cannot be called from a running event loop`.
    """

    @pytest.mark.asyncio
    async def test_async_url_from_running_loop_raises_typed_error(self, tmp_path: Path) -> None:
        """Inside pytest-asyncio (running loop), configure_env's
        async path raises the actionable RuntimeError naming the
        public alternative."""
        from sqlalchemy import MetaData

        from mgf.alembic import configure_env

        metadata = MetaData()
        # An async URL — would otherwise route through asyncio.run().
        url = f"sqlite+aiosqlite:///{tmp_path / 'x.db'}"

        # We need configure_env's offline-mode check to return False
        # so we reach the async dispatch path. The alembic.context
        # global isn't configured when invoked outside the CLI, so
        # we monkeypatch is_offline_mode.
        from unittest.mock import patch

        with (
            patch("alembic.context.is_offline_mode", return_value=False),
            pytest.raises(OperationError, match="running event loop"),
        ):
            configure_env(database_url=url, target_metadata=metadata)

    @pytest.mark.asyncio
    async def test_running_loop_error_names_public_alternative(self, tmp_path: Path) -> None:
        """The error message points at `run_async_migrations` — the
        public symbol embedded callers should use instead.
        Catches a future refactor that drops the actionable hint."""
        from sqlalchemy import MetaData

        from mgf.alembic import configure_env

        metadata = MetaData()
        url = f"sqlite+aiosqlite:///{tmp_path / 'x.db'}"

        from unittest.mock import patch

        with (
            patch("alembic.context.is_offline_mode", return_value=False),
            pytest.raises(OperationError) as exc_info,
        ):
            configure_env(database_url=url, target_metadata=metadata)

        msg = str(exc_info.value)
        assert "run_async_migrations" in msg, (
            f"RA-01: error message no longer names the public alternative. Message: {msg!r}"
        )
        assert "RA-01" in msg, (
            f"RA-01: error message no longer cross-references the tracker entry. Message: {msg!r}"
        )

    def test_sync_url_from_sync_context_unaffected(self, tmp_path: Path) -> None:
        """Sanity: the standard alembic-CLI shape (sync caller,
        sync or async URL) still works. The guard only fires when
        we'd be calling asyncio.run() from an already-running loop."""
        # Sync URL — never reaches the guard (which is on the async
        # branch). Routes through _run_online_sync.
        db_path = tmp_path / "test.db"
        url = f"sqlite:///{db_path}"
        config = _build_project(tmp_path, url=url)
        command.upgrade(config, "head")  # no exception


class TestRunAsyncMigrationsPublicEntryPoint:
    """RA-01 follow-up: `run_async_migrations` is the public async
    entry point. Embedded callers awaiting it directly bypass the
    `configure_env` -> `asyncio.run()` shape entirely."""

    def test_exported_from_package(self) -> None:
        import mgf.alembic

        assert "run_async_migrations" in mgf.alembic.__all__
        assert callable(mgf.alembic.run_async_migrations)

    @pytest.mark.asyncio
    @pytest.mark.filterwarnings("ignore::pytest.PytestUnraisableExceptionWarning")
    async def test_run_async_migrations_works_from_running_loop(self, tmp_path: Path) -> None:
        """Counter-test: when an async caller uses the public
        primitive directly, the running-loop case is the happy
        path — no RuntimeError. We verify by spinning up a real
        sqlite+aiosqlite migration through the helper."""
        import gc

        from sqlalchemy import Column, Integer, MetaData, String, Table

        from mgf.alembic import run_async_migrations

        db_path = tmp_path / "async-direct.db"
        url = f"sqlite+aiosqlite:///{db_path}"

        metadata = MetaData()
        Table(
            "users",
            metadata,
            Column("id", Integer, primary_key=True),
            Column("name", String(64), nullable=False),
        )

        # The helper expects alembic.context configured (it would be
        # by the alembic CLI). For an embedded test, we can't easily
        # set that up — but we CAN verify the function is reachable +
        # awaitable without the running-loop RuntimeError. A real
        # embedded use case configures alembic.context first.
        from unittest.mock import patch

        # We patch _do_run_migrations to a no-op (the real path
        # needs alembic.context.configure already set up); the
        # async-engine creation + connection acquisition exercise
        # what we care about: that the public path is awaitable
        # from a running loop without raising the asyncio.run guard.
        with patch(
            "mgf.alembic._env._do_run_migrations",
            return_value=None,
        ):
            await run_async_migrations(
                url=url,
                target_metadata=metadata,
            )

        gc.collect()


class TestNamingConventionMutation:
    """RA-02: `configure_env(naming_convention=...)` mutates the
    passed `target_metadata`. The mutation is the documented
    behaviour (alembic needs the convention ON the metadata for
    autogenerate). This regression test pins the contract — a
    future refactor that copies the metadata internally would
    fail it (and require updating the docstring)."""

    def test_naming_convention_set_on_metadata(self, tmp_path: Path) -> None:
        from sqlalchemy import MetaData

        from mgf.alembic import configure_env

        metadata = MetaData()
        assert metadata.naming_convention != {"pk": "pk_%(table_name)s"}, (
            "Pre-condition broken: metadata already has the test convention"
        )

        convention = {"pk": "pk_%(table_name)s"}
        # Use offline-mode SQL emit so we don't need a real DB.
        # The mutation happens before the offline-vs-online branch.
        from unittest.mock import patch

        with (
            patch("alembic.context.is_offline_mode", return_value=True),
            patch("mgf.alembic._env._run_offline"),
        ):
            configure_env(
                database_url="sqlite:///x",
                target_metadata=metadata,
                naming_convention=convention,
            )

        # The mutation IS the contract — pinned here so a future
        # refactor that copies the metadata internally fails AND
        # forces a docstring update before merge.
        assert metadata.naming_convention == convention, (
            "RA-02: naming_convention is no longer set on the passed "
            "target_metadata. If this is intentional, update the "
            "Side effect (RA-02) docstring section."
        )

    def test_naming_convention_none_does_not_mutate(self, tmp_path: Path) -> None:
        """When naming_convention is None (the default), the metadata
        is NOT touched. Pins the conditional."""
        from sqlalchemy import MetaData

        from mgf.alembic import configure_env

        metadata = MetaData()
        before = metadata.naming_convention

        from unittest.mock import patch

        with (
            patch("alembic.context.is_offline_mode", return_value=True),
            patch("mgf.alembic._env._run_offline"),
        ):
            configure_env(
                database_url="sqlite:///x",
                target_metadata=metadata,
            )

        assert metadata.naming_convention is before, (
            "RA-02: naming_convention=None (default) should not mutate the metadata."
        )


class TestAsyncUrlDetection:
    @pytest.mark.parametrize(
        "url",
        [
            "postgresql+asyncpg://user:pass@host/db",
            "mysql+aiomysql://user:pass@host/db",
            "sqlite+aiosqlite:///path",
            "postgresql+psycopg://user:pass@host/db",
            "mysql+asyncmy://user:pass@host/db",
        ],
    )
    def test_known_async_prefixes(self, url: str) -> None:
        from mgf.alembic._env import _ASYNC_URL_PREFIXES

        assert any(url.startswith(prefix) for prefix in _ASYNC_URL_PREFIXES)

    @pytest.mark.parametrize(
        "url",
        [
            "postgresql://user:pass@host/db",
            "postgresql+psycopg2://user:pass@host/db",
            "mysql+pymysql://user:pass@host/db",
            "sqlite:///path",
        ],
    )
    def test_known_sync_prefixes_not_in_async_list(self, url: str) -> None:
        from mgf.alembic._env import _ASYNC_URL_PREFIXES

        assert not any(url.startswith(prefix) for prefix in _ASYNC_URL_PREFIXES)
