from __future__ import annotations

import asyncio
import contextvars
import datetime
from types import SimpleNamespace

import pytest
import sqlalchemy as sa
from pgvector.sqlalchemy import HALFVEC, Vector
from sqlalchemy.dialects.postgresql import insert as postgresql_insert
from sqlalchemy.dialects.sqlite import insert as sqlite_insert
from sqlalchemy.ext.asyncio import create_async_engine
from sqlalchemy.ext.asyncio import async_object_session
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, registry, relationship, with_loader_criteria
from sqlalchemy.sql import quoted_name

from langbot.pkg.core.task_boundary import create_detached_task
from langbot.pkg.entity.persistence.cloud_directory import (
    DirectoryProjectionInbox,
    DirectoryProjectionState,
)
from langbot.pkg.entity.persistence.user import User
from langbot.pkg.persistence.mgr import PersistenceManager, PersistenceMode
from langbot.pkg.persistence.tenant_uow import (
    CrossScopeTransactionError,
    PersistenceScopeKind,
    ScopedSessionTransactionError,
    TenantScopedAsyncSession,
    TenantScopedSyncSession,
    TenantScopeRequiredError,
    TenantUnitOfWork,
    TransactionRollbackOnlyError,
    _validate_scoped_statement_call,
)


pytestmark = pytest.mark.asyncio


async def test_tenant_uow_reports_pool_timeout_during_transaction_admission(monkeypatch) -> None:
    engine = create_async_engine('sqlite+aiosqlite:///:memory:')
    pool_timeouts = 0

    def record_pool_timeout() -> None:
        nonlocal pool_timeouts
        pool_timeouts += 1

    async def fail_transaction_start(self, capability):
        del self, capability
        raise sa.exc.TimeoutError('pool exhausted')

    monkeypatch.setattr(TenantScopedAsyncSession, '_start_owned_transaction', fail_transaction_start)
    try:
        with pytest.raises(sa.exc.TimeoutError, match='pool exhausted'):
            async with TenantUnitOfWork(
                engine,
                '10000000-0000-0000-0000-000000000001',
                on_pool_timeout=record_pool_timeout,
            ):
                pass
    finally:
        await engine.dispose()

    assert pool_timeouts == 1


def _on_conflict_statement(*, update_value, update_key='value', index_element=None):
    table = sa.table('conflict_rows', sa.column('id'), sa.column('value'))
    if index_element is None:
        index_element = table.c.id
    return (
        sqlite_insert(table)
        .values(id=1, value=1)
        .on_conflict_do_update(
            index_elements=[index_element],
            set_={update_key: update_value},
        )
    )


def _on_conflict_constraint_statement(*, constraint):
    table = sa.table('conflict_rows', sa.column('id'), sa.column('value'))
    return (
        postgresql_insert(table)
        .values(id=1, value=1)
        .on_conflict_do_update(
            constraint=constraint,
            set_={'value': 2},
        )
    )


def _multi_value_statement(*, value, value_key='value'):
    table = sa.table('multi_value_rows', sa.column('id'), sa.column('value'))
    return sa.insert(table).values([{'id': 1, value_key: value}])


class _SpoofedCount(sa.sql.functions.FunctionElement):
    name = 'count'
    inherit_cache = True


class _UntrustedCastType(sa.types.UserDefinedType):
    def get_col_spec(self, **kwargs) -> str:
        del kwargs
        return 'INTEGER'


async def test_sqlite_tenant_uow_commits_and_rolls_back() -> None:
    engine = create_async_engine('sqlite+aiosqlite:///:memory:')
    table = sa.Table(
        'uow_rows',
        sa.MetaData(),
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('workspace_uuid', sa.String(36), nullable=False),
    )
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        async with TenantUnitOfWork(engine, 'workspace-a') as uow:
            await uow.execute(sa.insert(table).values(id=1, workspace_uuid='workspace-a'))

        with pytest.raises(RuntimeError, match='roll back'):
            async with TenantUnitOfWork(engine, 'workspace-a') as uow:
                await uow.execute(sa.insert(table).values(id=2, workspace_uuid='workspace-a'))
                raise RuntimeError('roll back this transaction')

        async with engine.connect() as conn:
            rows = (await conn.execute(sa.select(table.c.id).order_by(table.c.id))).scalars().all()
        assert rows == [1]
    finally:
        await engine.dispose()


async def test_tenant_uow_is_single_use_and_requires_an_active_scope() -> None:
    engine = create_async_engine('sqlite+aiosqlite:///:memory:')
    uow = TenantUnitOfWork(engine, 'workspace-a')
    try:
        with pytest.raises(RuntimeError, match='not active'):
            _ = uow.session

        async with uow:
            assert uow.session.in_transaction()

        with pytest.raises(RuntimeError, match='cannot be reused'):
            async with uow:
                pass
    finally:
        await engine.dispose()


def test_persistence_mode_must_be_a_trusted_enum() -> None:
    with pytest.raises(TypeError, match='trusted PersistenceMode'):
        PersistenceManager(object(), mode='cloud_runtime')  # type: ignore[arg-type]

    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    assert manager.mode is PersistenceMode.CLOUD_RUNTIME


async def test_manager_reuses_one_session_and_rejects_cross_workspace(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "manager-uow.db"}')
    table = sa.Table(
        'manager_rows',
        sa.MetaData(),
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('workspace_uuid', sa.String(36), nullable=False),
    )
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        with pytest.raises(TenantScopeRequiredError, match='explicit Workspace or discovery'):
            await manager.execute_async(sa.select(table))

        async with manager.tenant_uow('workspace-a') as outer:
            await manager.execute_async(sa.insert(table).values(id=1, workspace_uuid='workspace-a'))
            async with manager.tenant_uow('workspace-a') as inner:
                assert inner.session is outer.session
                assert manager.current_session() is outer.session
                assert (await manager.execute_async(sa.select(table.c.id))).scalar_one() == 1
            with pytest.raises(CrossScopeTransactionError, match='while workspace scope is active'):
                async with manager.tenant_uow('workspace-b'):
                    pass

        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table.c.id))).scalars().all() == [1]
    finally:
        await engine.dispose()


async def test_directory_projection_uow_is_instance_scoped_and_not_workspace_nestable(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "directory-projection-uow.db"}')
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    fingerprint = 'a' * 64
    try:
        async with engine.begin() as conn:
            await conn.run_sync(DirectoryProjectionState.__table__.create)
            await conn.run_sync(DirectoryProjectionInbox.__table__.create)

        async with manager.directory_projection_uow('instance-a') as uow:
            assert manager.current_scope() is not None
            assert manager.current_scope().kind is PersistenceScopeKind.DIRECTORY_PROJECTION
            assert manager.current_scope().settings == (('langbot.directory_instance_uuid', 'instance-a'),)
            manager.require_current_session(PersistenceScopeKind.DIRECTORY_PROJECTION)
            uow.session.add(
                DirectoryProjectionState(
                    instance_uuid='instance-a',
                    cursor=1,
                    snapshot_fingerprint=fingerprint,
                    last_applied_at=datetime.datetime.now(datetime.UTC),
                )
            )
            uow.session.add(
                DirectoryProjectionInbox(
                    instance_uuid='instance-a',
                    event_uuid='00000000-0000-0000-0000-000000000001',
                    cursor=1,
                    event_type='workspace.changed',
                    revision=1,
                    fingerprint=fingerprint,
                )
            )
            with pytest.raises(CrossScopeTransactionError, match='while directory_projection scope is active'):
                async with manager.tenant_uow('workspace-a'):
                    pass

        async with manager.tenant_scope('workspace-a'):
            with pytest.raises(CrossScopeTransactionError, match='while workspace scope is active'):
                async with manager.directory_projection_uow('instance-a'):
                    pass

        with pytest.raises(ValueError, match='must not be empty'):
            manager.directory_projection_uow('  ')
    finally:
        await engine.dispose()


async def test_manager_scoped_execute_preserves_core_row_and_scalar_contract(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "manager-result-contract.db"}')
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(User.__table__.create)
            await conn.execute(
                sa.insert(User).values(
                    uuid='account-a',
                    user='owner@example.com',
                    normalized_email='owner@example.com',
                    password='hashed-password',
                )
            )

        async with manager.tenant_uow('workspace-a') as uow:
            result = await manager.execute_async(sa.select(User))
            row = result.first()
            assert row is not None
            assert row.uuid == 'account-a'
            assert row.user == 'owner@example.com'

            scalar_result = await manager.execute_async(sa.select(User.uuid))
            assert scalar_result.scalar_one() == 'account-a'

            list_result = await manager.execute_async(sa.select(User.user))
            assert list_result.scalars().all() == ['owner@example.com']

            # Direct UoW execution remains an ORM API for code that opts into it.
            orm_result = await uow.execute(sa.select(User))
            assert orm_result.scalars().one().uuid == 'account-a'
    finally:
        await engine.dispose()


async def test_transaction_free_tenant_scope_opens_one_short_uow_per_database_call(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "short-scope.db"}')
    table = sa.Table(
        'short_scope_rows',
        sa.MetaData(),
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('workspace_uuid', sa.String(36), nullable=False),
    )
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        async with manager.tenant_scope('workspace-a'):
            assert manager.current_scope() is not None
            assert manager.current_scope().kind is PersistenceScopeKind.WORKSPACE
            assert manager.current_scope().settings == (('langbot.workspace_uuid', 'workspace-a'),)
            assert manager.current_session() is None

            await manager.execute_async(sa.insert(table).values(id=1, workspace_uuid='workspace-a'))
            assert manager.current_session() is None

            # The first statement has already committed. Long external waits
            # inside this boundary retain only identity, never a DB session.
            await asyncio.sleep(0)
            assert manager.current_session() is None

            assert (await manager.execute_async(sa.select(table.c.id))).scalar_one() == 1
            assert manager.current_session() is None

            async with manager.tenant_scope('workspace-a'):
                assert manager.current_session() is None
            with pytest.raises(CrossScopeTransactionError, match='while workspace scope is active'):
                async with manager.tenant_scope('workspace-b'):
                    pass
            with pytest.raises(CrossScopeTransactionError, match='while workspace scope is active'):
                async with manager.tenant_uow('workspace-b'):
                    pass

        assert manager.current_scope() is None
        with pytest.raises(TenantScopeRequiredError, match='explicit Workspace'):
            await manager.execute_async(sa.select(table))
    finally:
        await engine.dispose()


async def test_transaction_free_scope_requires_explicit_child_task_scope(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "short-scope-child.db"}')
    table = sa.Table('child_scope_rows', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True))
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        async with manager.tenant_scope('workspace-a'):

            async def inherited_access() -> None:
                await manager.execute_async(sa.select(table))

            with pytest.raises(CrossScopeTransactionError, match='cannot be inherited by child tasks'):
                await asyncio.create_task(inherited_access())

            async def explicitly_scoped_access() -> None:
                async with manager.tenant_scope('workspace-a'):
                    await manager.execute_async(sa.insert(table).values(id=1))
                    assert manager.current_session() is None

            await asyncio.create_task(explicitly_scoped_access())
            assert manager.current_session() is None

        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table.c.id))).scalars().all() == [1]
    finally:
        await engine.dispose()


async def test_caught_nested_failure_marks_outer_transaction_rollback_only(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "rollback-only.db"}')
    table = sa.Table('rollback_rows', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True))
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        with pytest.raises(TransactionRollbackOnlyError, match='transaction was rolled back'):
            async with manager.tenant_uow('workspace-a'):
                await manager.execute_async(sa.insert(table).values(id=1))
                try:
                    async with manager.tenant_uow('workspace-a'):
                        raise ValueError('caught nested failure')
                except ValueError:
                    pass

        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table))).all() == []
    finally:
        await engine.dispose()


@pytest.mark.parametrize('executor_kind', ['manager', 'uow', 'session'])
async def test_caught_database_error_rolls_back_and_cancels_after_commit_gate(tmp_path, executor_kind: str) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / f"db-error-{executor_kind}.db"}')
    table = sa.Table('unique_rows', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True))
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        with pytest.raises(TransactionRollbackOnlyError, match='after-commit work was cancelled'):
            async with manager.tenant_uow('workspace-a') as uow:
                statement = sa.insert(table).values(id=1)
                if executor_kind == 'manager':
                    await manager.execute_async(statement)
                elif executor_kind == 'uow':
                    await uow.execute(statement)
                else:
                    await uow.session.execute(statement)

                gate = manager.create_after_commit_gate()
                assert gate is not None
                try:
                    if executor_kind == 'manager':
                        await manager.execute_async(statement)
                    elif executor_kind == 'uow':
                        await uow.execute(statement)
                    else:
                        await uow.session.execute(statement)
                except sa.exc.IntegrityError:
                    pass

        assert gate.cancelled()
        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table))).all() == []
    finally:
        await engine.dispose()


async def test_child_task_must_open_its_own_explicit_uow(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "child-task.db"}')
    table = sa.Table(
        'child_rows',
        sa.MetaData(),
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('workspace_uuid', sa.String(36), nullable=False),
    )
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        async with manager.tenant_uow('workspace-a'):

            async def inherited_access() -> None:
                await manager.execute_async(sa.select(table))

            with pytest.raises(CrossScopeTransactionError, match='cannot be inherited by child tasks'):
                await asyncio.create_task(inherited_access())

            async def explicitly_scoped_access() -> None:
                async with manager.tenant_uow('workspace-a'):
                    await manager.execute_async(sa.insert(table).values(id=2, workspace_uuid='workspace-a'))

            await asyncio.create_task(explicitly_scoped_access())

        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table.c.id))).scalars().all() == [2]
    finally:
        await engine.dispose()


async def test_captured_session_rejects_child_task_database_access(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "captured-session-child.db"}')
    table = sa.Table('captured_session_rows', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True))
    mapper_registry = registry()

    class CapturedSessionRow:
        pass

    mapper_registry.map_imperatively(CapturedSessionRow, table)
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        async with manager.tenant_uow('workspace-a') as uow:
            captured_session = uow.session
            captured_execute = captured_session.execute
            captured_add = captured_session.add
            await captured_session.execute(sa.insert(table).values(id=1))

            async def inherited_session_write() -> None:
                await captured_execute(sa.insert(table).values(id=2))

            with pytest.raises(CrossScopeTransactionError, match='cannot be inherited by child tasks'):
                await asyncio.create_task(inherited_session_write())

            async def inherited_session_mutation() -> None:
                captured_add(CapturedSessionRow(id=2))

            with pytest.raises(CrossScopeTransactionError, match='cannot be inherited by child tasks'):
                await asyncio.create_task(inherited_session_mutation())

            # The rejected child never touched the connection and therefore
            # must not poison valid work still owned by the parent task.
            await captured_session.execute(sa.insert(table).values(id=3))

        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table.c.id).order_by(table.c.id))).scalars().all() == [1, 3]
    finally:
        await engine.dispose()


async def test_captured_session_is_permanently_inactive_after_uow_exit(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "captured-session-exit.db"}')
    table = sa.Table('captured_session_exit_rows', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True))
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        async with manager.tenant_uow('workspace-a') as uow:
            captured_session = uow.session
            captured_execute = captured_session.execute
            await captured_execute(sa.insert(table).values(id=1))

        with pytest.raises(ScopedSessionTransactionError, match='no longer active'):
            await captured_session.execute(sa.select(table))
        with pytest.raises(ScopedSessionTransactionError, match='no longer active'):
            await captured_execute(sa.select(table))
        with pytest.raises(ScopedSessionTransactionError, match='no longer active'):
            captured_session.in_transaction()

        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table.c.id))).scalars().all() == [1]
    finally:
        await engine.dispose()


@pytest.mark.parametrize(
    'operation',
    [
        'begin',
        'commit',
        'rollback',
        'close',
        'close_all',
        'connection',
        'get_bind',
        'bind',
        'sync_session',
        'stream',
        'stream_scalars',
    ],
)
async def test_scoped_session_cannot_escape_uow_transaction_lifecycle(tmp_path, operation: str) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / f"session-escape-{operation}.db"}')
    table = sa.Table('session_escape_rows', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True))
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        with pytest.raises(TransactionRollbackOnlyError, match='transaction was rolled back'):
            async with manager.tenant_uow('workspace-a') as uow:
                await uow.execute(sa.insert(table).values(id=1))
                with pytest.raises(ScopedSessionTransactionError, match=f'direct {operation}'):
                    if operation == 'begin':
                        uow.session.begin()
                    elif operation == 'commit':
                        await uow.session.commit()
                    elif operation == 'rollback':
                        await uow.session.rollback()
                    elif operation == 'close':
                        await uow.session.close()
                    elif operation == 'close_all':
                        await uow.session.close_all()
                    elif operation == 'connection':
                        await uow.session.connection()
                    elif operation == 'get_bind':
                        uow.session.get_bind().connect()
                    elif operation == 'bind':
                        _ = uow.session.bind
                    elif operation == 'sync_session':
                        _ = uow.session.sync_session
                    elif operation == 'stream':
                        await uow.session.stream(sa.select(table))
                    else:
                        await uow.session.stream_scalars(sa.select(table.c.id))

        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table))).all() == []
    finally:
        await engine.dispose()


async def test_scoped_session_no_autoflush_keeps_the_sync_proxy_private(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "no-autoflush.db"}')
    table = sa.Table('no_autoflush_rows', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True))
    mapper_registry = registry()

    class NoAutoflushRow:
        pass

    mapper_registry.map_imperatively(NoAutoflushRow, table)
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        async with manager.tenant_uow('workspace-a') as uow:
            assert uow.session.autoflush is True
            with uow.session.no_autoflush:
                assert uow.session.autoflush is False
                uow.session.add(NoAutoflushRow(id=1))
            assert uow.session.autoflush is True

        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table.c.id))).scalars().all() == [1]
    finally:
        await engine.dispose()


@pytest.mark.parametrize('access_kind', ['async_instance', 'global_sync'])
async def test_orm_object_cannot_expose_the_uow_sync_session(tmp_path, access_kind: str) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / f"object-session-{access_kind}.db"}')
    table = sa.Table('object_session_rows', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True))
    mapper_registry = registry()

    class ObjectSessionRow:
        pass

    mapper_registry.map_imperatively(ObjectSessionRow, table)
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        with pytest.raises(TransactionRollbackOnlyError, match='transaction was rolled back'):
            async with manager.tenant_uow('workspace-a') as uow:
                row = ObjectSessionRow(id=1)
                uow.session.add(row)
                await uow.session.flush()
                assert async_object_session(row) is uow.session
                gate = manager.create_after_commit_gate()
                assert gate is not None

                if access_kind == 'async_instance':
                    with pytest.raises(ScopedSessionTransactionError, match='object_session access'):
                        uow.session.object_session(row)
                else:
                    sync_session = sa.orm.object_session(row)
                    assert sync_session is not None
                    with pytest.raises(ScopedSessionTransactionError, match='synchronous Session access'):
                        sync_session.rollback()

        assert gate.cancelled()
        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table))).all() == []
    finally:
        await engine.dispose()


@pytest.mark.parametrize('event_name', ['do_orm_execute', 'before_flush'])
async def test_orm_session_events_fail_closed_before_callbacks_run(tmp_path, event_name: str) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / f"orm-event-{event_name}.db"}')
    table = sa.Table('orm_event_rows', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True))
    mapper_registry = registry()

    class EventRow:
        pass

    mapper_registry.map_imperatively(EventRow, table)
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    callback_called = False
    listener_registered = False

    def do_orm_execute_escape(state) -> None:
        nonlocal callback_called
        callback_called = True
        state.session.connection().exec_driver_sql('COMMIT')

    def before_flush_escape(session, flush_context, instances) -> None:
        nonlocal callback_called
        del flush_context, instances
        callback_called = True
        session.bind.connect()

    listener = do_orm_execute_escape if event_name == 'do_orm_execute' else before_flush_escape
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        with pytest.raises(TransactionRollbackOnlyError, match='transaction was rolled back'):
            async with manager.tenant_uow('workspace-a') as uow:
                await uow.execute(sa.insert(table).values(id=1))
                sa.event.listen(TenantScopedSyncSession, event_name, listener)
                listener_registered = True
                with pytest.raises(ScopedSessionTransactionError, match=f'event listener {event_name}'):
                    if event_name == 'do_orm_execute':
                        await uow.session.execute(sa.select(table))
                    else:
                        uow.session.add(EventRow(id=2))
                        await uow.session.flush()

        assert not callback_called
        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table))).all() == []
    finally:
        if listener_registered:
            sa.event.remove(TenantScopedSyncSession, event_name, listener)
        await engine.dispose()


async def test_pre_registered_orm_session_event_prevents_uow_start() -> None:
    engine = create_async_engine('sqlite+aiosqlite:///:memory:')
    callback_called = False

    def after_transaction_create_escape(session, transaction) -> None:
        nonlocal callback_called
        del session, transaction
        callback_called = True

    sa.event.listen(TenantScopedSyncSession, 'after_transaction_create', after_transaction_create_escape)
    try:
        with pytest.raises(ScopedSessionTransactionError, match='event listener after_transaction_create'):
            async with TenantUnitOfWork(engine, 'workspace-a'):
                pass
        assert not callback_called
    finally:
        sa.event.remove(TenantScopedSyncSession, 'after_transaction_create', after_transaction_create_escape)
        await engine.dispose()


async def test_explicit_async_refresh_loads_relationship_without_exposing_sync_session(tmp_path) -> None:
    class Base(DeclarativeBase):
        pass

    class Parent(Base):
        __tablename__ = 'async_attr_parents'

        id: Mapped[int] = mapped_column(primary_key=True)
        children: Mapped[list[Child]] = relationship(back_populates='parent')

    class Child(Base):
        __tablename__ = 'async_attr_children'

        id: Mapped[int] = mapped_column(primary_key=True)
        parent_id: Mapped[int] = mapped_column(sa.ForeignKey('async_attr_parents.id'))
        parent: Mapped[Parent] = relationship(back_populates='children')

    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "async-attrs.db"}')
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(Base.metadata.create_all)

        async with manager.tenant_uow('workspace-a') as uow:
            uow.session.add(Parent(id=1, children=[Child(id=1)]))

        async with manager.tenant_uow('workspace-a') as uow:
            parent = await uow.session.get(Parent, 1)
            assert parent is not None
            assert 'children' not in parent.__dict__
            await uow.session.refresh(parent, ['children'])
            assert [child.id for child in parent.children] == [1]
    finally:
        await engine.dispose()


@pytest.mark.parametrize(
    ('statement', 'params', 'keyword_call'),
    [
        (sa.text('ROLLBACK'), None, False),
        (sa.text('/* caller comment */ COMMIT'), None, False),
        (sa.text('SET LOCAL langbot.workspace_uuid = :value'), {'value': 'workspace-b'}, False),
        (sa.text("SET LOCAL langbot . workspace_uuid = 'workspace-b'"), None, False),
        (sa.text("SET/**/ LOCAL /**/ langbot . workspace_uuid = 'workspace-b'"), None, False),
        (
            sa.text('SELECT set_config(:setting_name, :setting_value, true)'),
            {'setting_name': 'langbot.workspace_uuid', 'setting_value': 'workspace-b'},
            False,
        ),
        (
            sa.text('SELECT set_config/**/(:setting_name, :setting_value, true)'),
            {'setting_name': 'langbot.workspace_uuid', 'setting_value': 'workspace-b'},
            False,
        ),
        (
            sa.text('SELECT "set_config"(:setting_name, :setting_value, true)'),
            {'setting_name': 'langbot.workspace_uuid', 'setting_value': 'workspace-b'},
            False,
        ),
        (sa.text("DO $$ BEGIN PERFORM set_config('langbot.workspace_uuid', 'workspace-b', true); END $$"), None, False),
        (sa.text('CALL tenant_scope_escape()'), None, False),
        (sa.text('CREATE FUNCTION tenant_scope_escape() RETURNS void LANGUAGE SQL AS $$ SELECT 1 $$'), None, False),
        (sa.text('SELECT * INTO TEMP leaked_rows FROM sql_escape_rows'), None, False),
        (sa.text('SELECT * INTO pg_temp.leaked_rows FROM sql_escape_rows'), None, False),
        (sa.text('DECLARE leaked_rows CURSOR WITH HOLD FOR SELECT * FROM sql_escape_rows'), None, False),
        (sa.text('FETCH ALL FROM leaked_rows'), None, False),
        (sa.text('PREPARE scope_escape AS SELECT 1'), None, False),
        (sa.text('EXECUTE scope_escape'), None, False),
        (sa.text('EXPLAIN (ANALYZE true) EXECUTE scope_escape'), None, False),
        (sa.text('LISTEN tenant_scope_escape'), None, False),
        (sa.text("NOTIFY tenant_scope_escape, 'payload'"), None, False),
        (sa.text('LOCK TABLE sql_escape_rows'), None, False),
        (sa.text('SELECT pg_advisory_lock(123)'), None, False),
        (sa.text('VALUES (pg_try_advisory_lock(123))'), None, False),
        (sa.text('EXPLAIN (ANALYZE TRUE) SELECT pg_advisory_lock(123)'), None, False),
        (sa.text("SELECT pg_notify('tenant_scope_escape', 'payload')"), None, False),
        (sa.text("SELECT lo_from_bytea(0, decode('AA==', 'base64'))"), None, False),
        (sa.text('SELECT lo_get(123)'), None, False),
        (sa.text("SELECT currval('shared_sequence')"), None, False),
        (sa.text('SELECT lastval()'), None, False),
        (
            sa.select(
                sa.func.query_to_xml(
                    sa.literal('SELECT 1'),
                    sa.literal(True),
                    sa.literal(False),
                    sa.literal(''),
                )
            ),
            None,
            False,
        ),
        (sa.select(sa.func.ts_stat(sa.literal('SELECT 1'))), None, False),
        (sa.select(sa.func.pg_catalog.count()), None, False),
        (sa.select(_SpoofedCount()), None, False),
        (sa.select(sa.literal_column('1')), None, False),
        (sa.select(sa.text('1')), None, False),
        (sa.select(sa.bindparam('value', 1, literal_execute=True)), None, False),
        (sa.select(sa.bindparam('value', 1, type_=_UntrustedCastType())), None, False),
        (sa.select(sa.column('value').op('@@')(sa.literal('query'))), None, False),
        (
            sa.select(
                sa.sql.expression.UnaryExpression(
                    sa.literal(True),
                    operator=sa.sql.operators.custom_op('unsafe_prefix'),
                )
            ),
            None,
            False,
        ),
        (
            sa.select(
                sa.sql.expression.UnaryExpression(
                    sa.literal(True),
                    modifier=sa.sql.operators.custom_op('unsafe_suffix'),
                )
            ),
            None,
            False,
        ),
        (sa.select(sa.extract('year', sa.literal('2026-01-01'))), None, False),
        (sa.select(sa.cast(sa.literal(1), _UntrustedCastType())), None, False),
        (
            sa.select(User).options(with_loader_criteria(User, sa.literal_column('1 = 1'))),
            None,
            False,
        ),
        (sa.select(sa.table(quoted_name('forced unquoted table', quote=False))), None, False),
        (sa.select(sa.literal(1).label(quoted_name('forced unquoted label', quote=False))), None, False),
        (
            sa.select(sa.collate(sa.column('value'), quoted_name('forced unquoted collation', quote=False))),
            None,
            False,
        ),
        (sa.select(sa.literal(1)).prefix_with('/* caller prefix */'), None, False),
        (sa.select(sa.literal(1)).suffix_with('FOR UPDATE'), None, False),
        (sa.select(sa.literal(1)).with_statement_hint('caller hint'), None, False),
        (
            _on_conflict_statement(
                update_value=sa.func.query_to_xml(
                    sa.literal('SELECT 1'),
                    sa.literal(True),
                    sa.literal(False),
                    sa.literal(''),
                )
            ),
            None,
            False,
        ),
        (_on_conflict_statement(update_value=sa.text('set_config(:name, :value, true)')), None, False),
        (
            _on_conflict_statement(
                update_value=1,
                update_key=quoted_name('forced unquoted update', quote=False),
            ),
            None,
            False,
        ),
        (
            _on_conflict_statement(
                update_value=1,
                index_element=quoted_name('forced unquoted target', quote=False),
            ),
            None,
            False,
        ),
        (
            _on_conflict_constraint_statement(constraint=quoted_name('forced unquoted constraint', quote=False)),
            None,
            False,
        ),
        (_multi_value_statement(value=sa.func.ts_stat(sa.literal('SELECT 1'))), None, False),
        (_multi_value_statement(value=sa.text('set_config(:name, :value, true)')), None, False),
        (
            _multi_value_statement(
                value=1,
                value_key=quoted_name('forced unquoted batch key', quote=False),
            ),
            None,
            False,
        ),
        (sa.values(sa.column('value')).data([(sa.func.ts_stat(sa.literal('SELECT 1')),)]), None, False),
        (
            sa.insert(sa.table('rows', sa.column('value'))).from_select(
                ['value'],
                sa.select(sa.literal(1)),
            ),
            None,
            False,
        ),
        (sa.text('ROLLBACK'), None, True),
        (
            sa.text('SELECT set_config(:setting_name, :setting_value, true)'),
            {'setting_name': 'langbot.workspace_uuid', 'setting_value': 'workspace-b'},
            True,
        ),
    ],
)
async def test_scoped_session_rejects_raw_or_unapproved_sql(
    tmp_path,
    statement,
    params,
    keyword_call: bool,
) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "sql-transaction-escape.db"}')
    table = sa.Table('sql_escape_rows', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True))
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        with pytest.raises(TransactionRollbackOnlyError, match='after-commit work was cancelled'):
            async with manager.tenant_uow('workspace-a') as uow:
                await uow.execute(sa.insert(table).values(id=1))
                gate = manager.create_after_commit_gate()
                assert gate is not None
                with pytest.raises(ScopedSessionTransactionError):
                    if keyword_call:
                        await uow.session.execute(statement=statement, params=params)
                    elif params is None:
                        await uow.session.execute(statement)
                    else:
                        await uow.session.execute(statement, params)

        assert gate.cancelled()
        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table))).all() == []
    finally:
        await engine.dispose()


@pytest.mark.parametrize(
    'statement',
    [
        sa.select(sa.literal('set_config(')),
        sa.select(sa.func.count()),
        sa.select(sa.func.coalesce(sa.func.sum(sa.literal(1)), sa.literal(0))),
        sa.select(
            sa.func.now(),
            sa.func.date_trunc('hour', sa.column('timestamp')),
            sa.func.length(sa.literal('value')),
            sa.func.nullif(sa.literal('value'), sa.literal('')),
            sa.func.strftime('%Y-%m-%d %H:00', sa.column('timestamp')),
        ),
        sa.select(sa.column('embedding').op('<=>')(sa.literal([0.1]))),
        sa.select(sa.cast(sa.column('embedding'), Vector(384))),
        sa.select(sa.cast(sa.column('embedding'), HALFVEC(3072))),
        sa.insert(sa.table('rows', sa.column('id'))).values(id=1),
        _multi_value_statement(value=1),
        _on_conflict_statement(update_value=sa.func.coalesce(sa.literal(1), sa.literal(0))),
    ],
)
async def test_scoped_sql_structure_allows_only_the_production_vocabulary(statement) -> None:
    _validate_scoped_statement_call((statement,), {})


async def test_scoped_session_executes_sqlite_strftime() -> None:
    engine = create_async_engine('sqlite+aiosqlite:///:memory:')
    try:
        async with TenantUnitOfWork(engine, 'workspace-a') as uow:
            result = await uow.session.execute(
                sa.select(sa.func.strftime('%Y-%m-%d %H:00', sa.literal('2026-08-28 03:45:00')))
            )

        assert result.scalar_one() == '2026-08-28 03:00'
    finally:
        await engine.dispose()


async def test_scoped_sql_rejects_public_execution_options() -> None:
    statement = sa.select(sa.literal(1))
    with pytest.raises(ScopedSessionTransactionError, match='execution options'):
        _validate_scoped_statement_call(
            (statement,),
            {'execution_options': {'schema_translate_map': {None: 'other_schema'}}},
        )


@pytest.mark.parametrize('operation', ['get', 'get_one', 'refresh', 'merge'])
async def test_scoped_orm_loaders_reject_public_query_options(operation: str) -> None:
    engine = create_async_engine('sqlite+aiosqlite:///:memory:')
    try:
        with pytest.raises(TransactionRollbackOnlyError, match='transaction was rolled back'):
            async with TenantUnitOfWork(engine, 'workspace-a') as uow:
                with pytest.raises(ScopedSessionTransactionError, match=operation):
                    if operation == 'get':
                        await uow.session.get(
                            User,
                            1,
                            execution_options={'schema_translate_map': {None: 'other_schema'}},
                        )
                    elif operation == 'get_one':
                        await uow.session.get_one(User, 1, options=[object()])
                    elif operation == 'refresh':
                        await uow.session.refresh(object(), with_for_update=True)
                    else:
                        await uow.session.merge(User(), options=[object()])
    finally:
        await engine.dispose()


async def test_scoped_get_rejects_empty_for_update_mapping() -> None:
    engine = create_async_engine('sqlite+aiosqlite:///:memory:')
    try:
        with pytest.raises(TransactionRollbackOnlyError, match='transaction was rolled back'):
            async with TenantUnitOfWork(engine, 'workspace-a') as uow:
                with pytest.raises(ScopedSessionTransactionError, match='get option with_for_update'):
                    await uow.session.get(User, 1, with_for_update={})
    finally:
        await engine.dispose()


@pytest.mark.parametrize('flush_kind', ['explicit', 'autoflush'])
async def test_scoped_orm_writes_reject_attribute_sql_expressions(tmp_path, flush_kind: str) -> None:
    class Base(DeclarativeBase):
        pass

    class Row(Base):
        __tablename__ = f'orm_expression_{flush_kind}'

        id: Mapped[int] = mapped_column(primary_key=True)
        value: Mapped[int] = mapped_column()

    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / f"orm-expression-{flush_kind}.db"}')
    try:
        async with engine.begin() as conn:
            await conn.run_sync(Base.metadata.create_all)

        with pytest.raises(TransactionRollbackOnlyError, match='transaction was rolled back'):
            async with TenantUnitOfWork(engine, 'workspace-a') as uow:
                uow.session.add(Row(id=1, value=sa.literal_column('40 + 2')))
                with pytest.raises(ScopedSessionTransactionError, match='ORM SQL expression'):
                    if flush_kind == 'explicit':
                        await uow.session.flush()
                    else:
                        await uow.session.execute(sa.select(Row.id))

        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(Row))).all() == []
    finally:
        await engine.dispose()


async def test_scoped_session_rejects_an_explicit_foreign_bind(tmp_path) -> None:
    primary = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "primary-bind.db"}')
    foreign = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "foreign-bind.db"}')
    table = sa.Table('bind_escape_rows', sa.MetaData(), sa.Column('id', sa.Integer, primary_key=True))
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: primary)
    try:
        for engine in (primary, foreign):
            async with engine.begin() as conn:
                await conn.run_sync(table.metadata.create_all)

        with pytest.raises(TransactionRollbackOnlyError, match='transaction was rolled back'):
            async with manager.tenant_uow('workspace-a') as uow:
                await uow.execute(sa.insert(table).values(id=1))
                with pytest.raises(ScopedSessionTransactionError, match='foreign database bind'):
                    await uow.session.execute(
                        sa.insert(table).values(id=2),
                        bind_arguments={'bind': foreign.sync_engine},
                    )

        for engine in (primary, foreign):
            async with engine.connect() as conn:
                assert (await conn.execute(sa.select(table))).all() == []
    finally:
        await primary.dispose()
        await foreign.dispose()


async def test_detached_task_starts_without_parent_scope_and_rolls_back_its_uow(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "detached-task.db"}')
    table = sa.Table(
        'detached_rows',
        sa.MetaData(),
        sa.Column('id', sa.Integer, primary_key=True),
        sa.Column('workspace_uuid', sa.String(36), nullable=False),
    )
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    try:
        async with engine.begin() as conn:
            await conn.run_sync(table.metadata.create_all)

        async with manager.tenant_uow('workspace-a'):

            async def detached_write() -> None:
                # Before the detached boundary this access raises
                # CrossScopeTransactionError because asyncio copies the
                # parent's ActiveScopedTransaction into this child task.
                assert manager.current_scope() is None
                async with manager.tenant_uow('workspace-a'):
                    await manager.execute_async(sa.insert(table).values(id=2, workspace_uuid='workspace-a'))
                    raise RuntimeError('roll back detached write')

            task = create_detached_task(detached_write())
            with pytest.raises(RuntimeError, match='roll back detached write'):
                await task

            await manager.execute_async(sa.insert(table).values(id=1, workspace_uuid='workspace-a'))

        async with engine.connect() as conn:
            assert (await conn.execute(sa.select(table.c.id))).scalars().all() == [1]
    finally:
        await engine.dispose()


async def test_after_commit_task_waits_for_commit_and_starts_with_empty_context(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "after-commit.db"}')
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    request_value = contextvars.ContextVar('after_commit_request_value', default=None)
    observed = []
    try:
        async with manager.tenant_uow('workspace-a'):
            token = request_value.set('request-scope')

            async def after_commit_work() -> None:
                observed.append((request_value.get(), manager.current_scope()))

            try:
                task = create_detached_task(
                    after_commit_work(),
                    after_commit_manager=manager,
                )
                await asyncio.sleep(0)
                assert observed == []
            finally:
                request_value.reset(token)

        await task
        assert observed == [(None, None)]
    finally:
        await engine.dispose()


async def test_after_commit_task_is_cancelled_and_coroutine_closed_on_rollback(tmp_path) -> None:
    engine = create_async_engine(f'sqlite+aiosqlite:///{tmp_path / "after-rollback.db"}')
    manager = PersistenceManager(object(), mode=PersistenceMode.CLOUD_RUNTIME)
    manager.db = SimpleNamespace(get_engine=lambda: engine)
    started = False

    async def should_not_start() -> None:
        nonlocal started
        started = True

    coro = should_not_start()
    try:
        with pytest.raises(RuntimeError, match='rollback request'):
            async with manager.tenant_uow('workspace-a'):
                task = create_detached_task(coro, after_commit_manager=manager)
                await asyncio.sleep(0)
                assert not task.done()
                raise RuntimeError('rollback request')

        await asyncio.sleep(0)
        assert task.cancelled()
        assert not started
        assert coro.cr_frame is None
    finally:
        await engine.dispose()
