"""Tests for ``mgf.sqlalchemy.create_engine``."""

from __future__ import annotations

import pytest
from sqlalchemy.ext.asyncio import AsyncEngine

from mgf.sqlalchemy import create_engine


class TestCreateEngine:
    def test_returns_async_engine(self) -> None:
        engine = create_engine("sqlite+aiosqlite:///:memory:")
        assert isinstance(engine, AsyncEngine)

    def test_sqlite_skips_pool_kwargs(self) -> None:
        # SQLite uses StaticPool / NullPool which reject pool_size /
        # max_overflow. The factory should detect SQLite and omit them.
        engine = create_engine(
            "sqlite+aiosqlite:///:memory:",
            pool_size=99,
            max_overflow=99,
        )
        assert isinstance(engine, AsyncEngine)

    def test_echo_passthrough(self) -> None:
        engine = create_engine("sqlite+aiosqlite:///:memory:", echo=True)
        assert engine.echo is True

    def test_extra_kwargs_forwarded(self) -> None:
        engine = create_engine(
            "sqlite+aiosqlite:///:memory:",
            connect_args={"check_same_thread": False},
        )
        assert isinstance(engine, AsyncEngine)


@pytest.mark.asyncio
class TestEngineActuallyConnects:
    async def test_in_memory_sqlite_round_trip(self) -> None:
        from sqlalchemy import text

        engine = create_engine("sqlite+aiosqlite:///:memory:")
        try:
            async with engine.connect() as conn:
                result = await conn.execute(text("SELECT 1 + 1"))
                row = result.scalar_one()
            assert row == 2
        finally:
            await engine.dispose()


class TestMakeSafeUrl:
    """SH-02: `make_safe_url` masks the password in connection URLs
    so the string is safe to log / surface in exception messages /
    crash reports. Used whenever the URL would otherwise reach an
    operator's screen or a persisted artifact."""

    def test_password_masked(self) -> None:
        from mgf.sqlalchemy import make_safe_url

        url = "postgresql+asyncpg://app_user:supersecret@db.internal:5432/myapp"
        safe = make_safe_url(url)

        assert "supersecret" not in safe, (
            f"SH-02: make_safe_url did not mask the password: {safe!r}"
        )
        # Other URL components stay — they're not secrets.
        assert "app_user" in safe
        assert "db.internal" in safe
        assert "myapp" in safe
        assert "asyncpg" in safe

    def test_no_password_url_round_trips(self) -> None:
        """A URL without an embedded password renders cleanly."""
        from mgf.sqlalchemy import make_safe_url

        url = "sqlite+aiosqlite:///./test.db"
        safe = make_safe_url(url)
        assert "sqlite" in safe
        assert "test.db" in safe

    def test_malformed_url_returns_marker(self) -> None:
        """Helper is safe to call on malformed input — returns a
        sentinel marker rather than raising. Important for error-
        path callers (an error path shouldn't fail because the URL
        couldn't be parsed)."""
        from mgf.sqlalchemy import make_safe_url

        safe = make_safe_url("not a url at all !! @@")
        assert isinstance(safe, str)
        # Either the sentinel or whatever make_url tolerates —
        # the contract is "doesn't raise".

    def test_does_not_raise_on_empty_string(self) -> None:
        from mgf.sqlalchemy import make_safe_url

        safe = make_safe_url("")
        assert isinstance(safe, str)

    def test_exported_from_package(self) -> None:
        """The helper is part of the public surface — consumers
        import it as `from mgf.sqlalchemy import make_safe_url`."""
        import mgf.sqlalchemy

        assert "make_safe_url" in mgf.sqlalchemy.__all__


class TestSqliteShapedUrlDetection:
    """RA-05: the SQLite-shape detection covers SQLite-shaped drivers.

    The check decides whether ``pool_size`` / ``max_overflow`` get
    forwarded to ``create_async_engine``. SQLite (and SQLite-shaped
    drivers like SQLCipher) use ``StaticPool`` / ``NullPool`` which
    REJECT those kwargs at construction time. If the detection
    misses a SQLite-shaped URL scheme, the engine fails to build.

    The check is implemented as a URL-prefix tuple. These tests pin
    the recognised set at the data layer (the test suite can't
    fully exercise engine construction for every driver — asyncpg,
    sqlcipher etc. aren't in our dev-deps — so we pin the
    constant + the documented behaviour).
    """

    def test_aiosqlite_url_works_with_pool_kwargs(self) -> None:
        """``sqlite+aiosqlite://...`` is recognised and pool kwargs
        are stripped — construction succeeds despite a pool_size
        that StaticPool would reject."""
        engine = create_engine(
            "sqlite+aiosqlite:///:memory:",
            pool_size=99,
            max_overflow=99,
        )
        assert isinstance(engine, AsyncEngine)

    def test_detection_uses_documented_prefix_constant(self) -> None:
        """The prefix tuple is exposed at module scope so future
        edits land in one place. A refactor that inlines the prefix
        list (losing the constant) gets caught here."""
        from mgf.sqlalchemy import _engine

        assert hasattr(_engine, "_SQLITE_SHAPED_URL_PREFIXES")
        prefixes = _engine._SQLITE_SHAPED_URL_PREFIXES
        # SQLite (both legacy sync URL form + async +driver shapes).
        assert "sqlite://" in prefixes
        assert "sqlite+" in prefixes
        # RA-05: SQLCipher (encrypted SQLite) — same pool semantics.
        assert "sqlcipher://" in prefixes
        assert "sqlcipher+" in prefixes

    @pytest.mark.parametrize(
        "url",
        [
            "sqlite://",
            "sqlite:///path/to/db.sqlite",
            "sqlite+aiosqlite:///:memory:",
            "sqlite+pysqlite:///foo.db",
            "sqlcipher://path/to/encrypted.db",
            "sqlcipher+pysqlcipher:///x.db",
            "sqlcipher+aiosqlcipher:///x.db",
        ],
    )
    def test_sqlite_shaped_prefix_match(self, url: str) -> None:
        """Pin: each documented SQLite-shaped URL matches the
        prefix tuple. If a future edit adds a new prefix variant
        the matrix expands here."""
        from mgf.sqlalchemy import _engine

        assert url.startswith(_engine._SQLITE_SHAPED_URL_PREFIXES), (
            f"RA-05: SQLite-shaped URL {url!r} not recognised by "
            f"_SQLITE_SHAPED_URL_PREFIXES; pool kwargs would be "
            f"incorrectly forwarded."
        )

    @pytest.mark.parametrize(
        "url",
        [
            "postgresql://user:pass@host/db",
            "postgresql+asyncpg://user:pass@host/db",
            "postgresql+psycopg://user:pass@host/db",
            "mysql+aiomysql://user:pass@host/db",
            "mysql+asyncmy://user:pass@host/db",
            "mariadb+asyncmy://user:pass@host/db",
        ],
    )
    def test_non_sqlite_shaped_prefix_no_match(self, url: str) -> None:
        """Pin: server-DB URLs DON'T match the SQLite-shape — pool
        kwargs flow through normally."""
        from mgf.sqlalchemy import _engine

        assert not url.startswith(_engine._SQLITE_SHAPED_URL_PREFIXES), (
            f"RA-05: server-DB URL {url!r} incorrectly matched the "
            f"SQLite-shape prefix; pool kwargs would be stripped."
        )


class TestExtraCollisionCheck:
    """RA-02: defensive collision check on `**extra`.

    Investigation: the tracker concern (a `**extra` dict that
    "silently overrides" explicit named kwargs) is actually
    prevented by Python's kwarg routing at the CALL layer —
    `create_engine(url, pool_size=20, **{"pool_size": 5})` raises
    `TypeError` from CPython before our function runs. The silent-
    override scenario can't occur from user code.

    The collision check inside the function body is defensive
    against future refactors. If a named parameter were removed
    from the signature, its key would suddenly route into `**extra`
    instead of producing a TypeError — at which point a consumer's
    old call shape would start silently using the default. The
    check turns that future regression into a loud ValueError.

    These tests pin the live shape: Python catches the collision
    at the call layer, AND non-colliding `**extra` keys flow
    through cleanly. The defensive check itself is documented in
    the source so a refactor that removes it has a clear pointer
    back to RA-02.
    """

    def test_python_kwarg_collision_raises_typeerror(self) -> None:
        """Python's own kwarg routing handles the user-facing
        collision — TypeError before our function runs. Pinning
        this here documents the live behaviour."""
        # Construct the colliding dict via a variable so ruff PIE804
        # doesn't see the literal `**{"k": v}` shape (which it
        # rejects as "unnecessary dict kwargs" — but the whole
        # point of the test IS that shape).
        legacy_kwargs = {"pool_size": 5}
        with pytest.raises(TypeError, match="multiple values"):
            create_engine(
                "postgresql+asyncpg://user:pass@host/db",
                pool_size=20,
                **legacy_kwargs,
            )

    def test_non_colliding_extra_passes(self) -> None:
        """`connect_args` is NOT a named kwarg → no collision.
        Should construct successfully."""
        engine = create_engine(
            "sqlite+aiosqlite:///:memory:",
            connect_args={"check_same_thread": False},
        )
        assert engine is not None

    def test_collision_check_present_in_source(self) -> None:
        """Documentation test: the defensive collision check IS
        in `create_engine`'s body. A refactor that removes it
        without thinking about RA-02 fails this assertion."""
        import inspect

        from mgf.sqlalchemy import _engine

        source = inspect.getsource(_engine.create_engine)
        assert "collisions = set(extra) & set(kwargs)" in source, (
            "RA-02: defensive collision check removed from "
            "create_engine. If this is intentional, update the "
            "docstring's '`**extra` MUST NOT collide' clause too."
        )
