"""Tests for user-scoped file system operations via GroverAsync.

Covers GroverAsync routing with UserScopedFileSystem backend, user isolation,
and shared access via @shared/ virtual namespace.

Unit tests for path resolution helpers live in test_user_scoped_fs.py.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import pytest
from sqlmodel import select

from grover.backends.database import DatabaseFileSystem
from grover.backends.user_scoped import UserScopedFileSystem
from grover.client import GroverAsync
from grover.exceptions import AuthenticationRequiredError
from grover.models.config import SessionConfig
from grover.models.database.file import FileModel
from grover.models.database.share import FileShareModel
from grover.models.internal.evidence import ListDirEvidence
from grover.worker import IndexingMode

if TYPE_CHECKING:
    from grover.models.internal.results import FileSearchResult

# ---------------------------------------------------------------------------
# Helpers for new result types
# ---------------------------------------------------------------------------


def _directories(result: FileSearchResult) -> list[str]:
    """Return paths of entries that are directories (via ListDirEvidence)."""
    dirs: list[str] = []
    for f in result.files:
        if f.is_directory:
            dirs.append(f.path)
            continue
        for e in f.evidence:
            if isinstance(e, ListDirEvidence) and e.is_directory:
                dirs.append(f.path)
                break
    return dirs


def _files_only(result: FileSearchResult) -> list[str]:
    """Return paths of entries that are NOT directories."""
    files: list[str] = []
    for f in result.files:
        if f.is_directory:
            continue
        is_dir = False
        for e in f.evidence:
            if isinstance(e, ListDirEvidence) and e.is_directory:
                is_dir = True
                break
        if not is_dir:
            files.append(f.path)
    return files


if TYPE_CHECKING:
    from pathlib import Path

    from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession


# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------


@pytest.fixture
async def auth_grover(async_engine: AsyncEngine, tmp_path: Path) -> GroverAsync:
    """GroverAsync with a single UserScopedFileSystem mount (no sharing service)."""
    from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

    backend = UserScopedFileSystem()
    session_factory = async_sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)

    g = GroverAsync(indexing_mode=IndexingMode.MANUAL)
    await g.add_mount(
        "/ws", filesystem=backend, session_config=SessionConfig(session_factory=session_factory, dialect="sqlite")
    )
    yield g  # type: ignore[misc]
    await g.close()


@pytest.fixture
async def shared_grover(async_engine: AsyncEngine, tmp_path: Path) -> GroverAsync:
    """GroverAsync with UserScopedFileSystem mount and sharing configured."""
    from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

    backend = UserScopedFileSystem(share_model=FileShareModel)
    session_factory = async_sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)

    g = GroverAsync(indexing_mode=IndexingMode.MANUAL)
    await g.add_mount(
        "/ws", filesystem=backend, session_config=SessionConfig(session_factory=session_factory, dialect="sqlite")
    )
    yield g  # type: ignore[misc]
    await g.close()


@pytest.fixture
async def regular_grover(async_engine: AsyncEngine, tmp_path: Path) -> GroverAsync:
    """GroverAsync with a single plain DatabaseFileSystem mount (no user scoping)."""
    from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker

    backend = DatabaseFileSystem()
    session_factory = async_sessionmaker(async_engine, class_=AsyncSession, expire_on_commit=False)

    g = GroverAsync(indexing_mode=IndexingMode.MANUAL)
    await g.add_mount(
        "/ws", filesystem=backend, session_config=SessionConfig(session_factory=session_factory, dialect="sqlite")
    )
    yield g  # type: ignore[misc]
    await g.close()


# ---------------------------------------------------------------------------
# Integration: GroverAsync read/write with UserScopedFileSystem backend
# ---------------------------------------------------------------------------


class TestAuthenticatedReadWrite:
    async def test_write_authenticated_mount(self, auth_grover: GroverAsync):
        result = await auth_grover.write("/ws/notes.md", "hello alice", user_id="alice")
        assert result.success is True

    async def test_read_authenticated_mount(self, auth_grover: GroverAsync):
        await auth_grover.write("/ws/notes.md", "hello alice", user_id="alice")
        result = await auth_grover.read("/ws/notes.md", user_id="alice")
        assert result.success is True
        assert result.file.content == "hello alice"

    async def test_read_authenticated_no_user_id_error(self, auth_grover: GroverAsync):
        with pytest.raises(AuthenticationRequiredError):
            await auth_grover.read("/ws/notes.md", user_id=None)

    async def test_write_sets_owner_id(self, auth_grover: GroverAsync, async_session: AsyncSession):
        await auth_grover.write("/ws/notes.md", "owned content", user_id="alice")

        # Query the file record directly to verify owner_id
        result = await async_session.execute(select(FileModel).where(FileModel.path == "/alice/notes.md"))
        file = result.scalar_one_or_none()
        assert file is not None
        assert file.owner_id == "alice"

    async def test_two_users_isolated(self, auth_grover: GroverAsync):
        """Two users write to the same virtual path, get different content."""
        await auth_grover.write("/ws/notes.md", "alice's notes", user_id="alice")
        await auth_grover.write("/ws/notes.md", "bob's notes", user_id="bob")

        r1 = await auth_grover.read("/ws/notes.md", user_id="alice")
        r2 = await auth_grover.read("/ws/notes.md", user_id="bob")

        assert r1.file.content == "alice's notes"
        assert r2.file.content == "bob's notes"

    async def test_user_cannot_see_other_users_files(self, auth_grover: GroverAsync):
        await auth_grover.write("/ws/secret.md", "alice only", user_id="alice")
        result = await auth_grover.read("/ws/secret.md", user_id="bob")
        assert result.success is False  # FileModel not found for bob

    async def test_regular_mount_ignores_user_id(self, regular_grover: GroverAsync):
        """Regular (non-user-scoped) mounts ignore user_id."""
        await regular_grover.write("/ws/notes.md", "shared content", user_id="alice")
        result = await regular_grover.read("/ws/notes.md", user_id="bob")
        assert result.success is True
        assert result.file.content == "shared content"

    async def test_write_read_roundtrip_authenticated(self, auth_grover: GroverAsync):
        """Full write-read roundtrip with path stripping on read."""
        await auth_grover.write("/ws/project/src/main.py", "print('hello')", user_id="alice")
        result = await auth_grover.read("/ws/project/src/main.py", user_id="alice")
        assert result.success is True
        assert result.file.content == "print('hello')"
        # path should be user-facing (no user prefix)
        assert result.file.path == "/ws/project/src/main.py"

    async def test_write_no_user_id_error(self, auth_grover: GroverAsync):
        result = await auth_grover.write("/ws/notes.md", "content", user_id=None)
        assert result.success is False
        assert "user_id is required" in result.message.lower()


# ---------------------------------------------------------------------------
# Integration: edit, delete, mkdir on user-scoped mount
# ---------------------------------------------------------------------------


class TestAuthenticatedOtherOps:
    async def test_edit_authenticated(self, auth_grover: GroverAsync):
        await auth_grover.write("/ws/notes.md", "hello world", user_id="alice")
        result = await auth_grover.edit("/ws/notes.md", "hello", "goodbye", user_id="alice")
        assert result.success is True
        read_result = await auth_grover.read("/ws/notes.md", user_id="alice")
        assert read_result.file.content == "goodbye world"

    async def test_delete_authenticated(self, auth_grover: GroverAsync):
        await auth_grover.write("/ws/notes.md", "content", user_id="alice")
        result = await auth_grover.delete("/ws/notes.md", user_id="alice")
        assert result.success is True

    async def test_mkdir_authenticated(self, auth_grover: GroverAsync):
        result = await auth_grover.mkdir("/ws/mydir", user_id="alice")
        assert result.success is True

    async def test_exists_authenticated(self, auth_grover: GroverAsync):
        await auth_grover.write("/ws/notes.md", "content", user_id="alice")
        assert (await auth_grover.exists("/ws/notes.md", user_id="alice")).message == "exists"
        assert (await auth_grover.exists("/ws/notes.md", user_id="bob")).message != "exists"

    async def test_get_info_authenticated(self, auth_grover: GroverAsync):
        await auth_grover.write("/ws/notes.md", "content", user_id="alice")
        info = await auth_grover.get_info("/ws/notes.md", user_id="alice")
        assert info.success
        assert info.file.path == "/ws/notes.md"

    async def test_get_info_other_user_none(self, auth_grover: GroverAsync):
        await auth_grover.write("/ws/notes.md", "content", user_id="alice")
        info = await auth_grover.get_info("/ws/notes.md", user_id="bob")
        assert not info.success

    async def test_copy_authenticated(self, auth_grover: GroverAsync):
        await auth_grover.write("/ws/a.md", "content", user_id="alice")
        result = await auth_grover.copy("/ws/a.md", "/ws/b.md", user_id="alice")
        assert result.success is True
        read_result = await auth_grover.read("/ws/b.md", user_id="alice")
        assert read_result.file.content == "content"

    async def test_move_authenticated(self, auth_grover: GroverAsync):
        await auth_grover.write("/ws/old.md", "content", user_id="alice")
        result = await auth_grover.move("/ws/old.md", "/ws/new.md", user_id="alice")
        assert result.success is True
        read_result = await auth_grover.read("/ws/new.md", user_id="alice")
        assert read_result.file.content == "content"

    async def test_list_dir_shows_shared_entry(self, auth_grover: GroverAsync):
        """User-scoped mount root listing includes virtual @shared/ entry."""
        await auth_grover.write("/ws/notes.md", "content", user_id="alice")
        result = await auth_grover.list_dir("/ws", user_id="alice")
        names = {p.rsplit("/", 1)[-1] for p in result.paths}
        assert "@shared" in names

    async def test_glob_authenticated(self, auth_grover: GroverAsync):
        await auth_grover.write("/ws/notes.md", "content", user_id="alice")
        await auth_grover.write("/ws/notes.md", "other", user_id="bob")
        result = await auth_grover.glob("*.md", "/ws", user_id="alice")
        assert result.success is True
        paths = set(result.paths)
        assert "/ws/notes.md" in paths
        # Bob's file should not appear
        assert len(result) == 1

    async def test_tree_authenticated(self, auth_grover: GroverAsync):
        await auth_grover.write("/ws/a.md", "a", user_id="alice")
        await auth_grover.write("/ws/b.md", "b", user_id="alice")
        result = await auth_grover.tree("/ws", user_id="alice")
        assert result.success is True
        paths = set(result.paths)
        assert "/ws/a.md" in paths
        assert "/ws/b.md" in paths

    async def test_regular_mount_all_ops_unchanged(self, regular_grover: GroverAsync):
        """Regular mount operations work with user_id (ignored)."""
        await regular_grover.write("/ws/notes.md", "content", user_id="alice")
        assert (await regular_grover.exists("/ws/notes.md", user_id="bob")).message == "exists"
        info = await regular_grover.get_info("/ws/notes.md", user_id="bob")
        assert info.success


# ---------------------------------------------------------------------------
# Integration: @shared access with SharingService
# ---------------------------------------------------------------------------


class TestSharedAccess:
    async def _create_share(
        self,
        grover: GroverAsync,
        async_session: AsyncSession,
        path: str,
        grantee_id: str,
        permission: str = "read",
        granted_by: str = "alice",
    ) -> None:
        """Helper to create a share record directly via USFS private method."""
        mount = grover._ctx.registry.get_mount("/ws")
        assert mount is not None
        backend = mount.filesystem
        assert isinstance(backend, UserScopedFileSystem)
        assert backend._share_model is not None
        await backend._create_share(async_session, path, grantee_id, permission, granted_by)
        await async_session.commit()

    async def test_read_shared_file(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Bob reads alice's file via @shared/ path with read share."""
        await shared_grover.write("/ws/notes.md", "alice's notes", user_id="alice")
        await self._create_share(shared_grover, async_session, "/alice/notes.md", "bob", "read")

        result = await shared_grover.read("/ws/@shared/alice/notes.md", user_id="bob")
        assert result.success is True
        assert result.file.content == "alice's notes"

    async def test_read_shared_no_permission(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Bob cannot read alice's file without a share."""
        await shared_grover.write("/ws/notes.md", "alice's notes", user_id="alice")

        with pytest.raises(PermissionError, match="Access denied"):
            await shared_grover.read("/ws/@shared/alice/notes.md", user_id="bob")

    async def test_write_shared_file_with_write_perm(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Bob writes to alice's file via @shared/ with write share."""
        await shared_grover.write("/ws/notes.md", "original", user_id="alice")
        await self._create_share(shared_grover, async_session, "/alice/notes.md", "bob", "write")

        result = await shared_grover.write("/ws/@shared/alice/notes.md", "updated by bob", user_id="bob")
        assert result.success is True

        # Alice sees bob's changes
        read_result = await shared_grover.read("/ws/notes.md", user_id="alice")
        assert read_result.file.content == "updated by bob"

    async def test_write_shared_file_read_only(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Bob cannot write to alice's file with only read share."""
        await shared_grover.write("/ws/notes.md", "original", user_id="alice")
        await self._create_share(shared_grover, async_session, "/alice/notes.md", "bob", "read")

        result = await shared_grover.write("/ws/@shared/alice/notes.md", "hacked", user_id="bob")
        assert result.success is False
        assert "access denied" in result.message.lower()

    async def test_edit_shared_file_with_write_perm(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Bob edits alice's file via @shared/ with write share."""
        await shared_grover.write("/ws/notes.md", "hello world", user_id="alice")
        await self._create_share(shared_grover, async_session, "/alice/notes.md", "bob", "write")

        result = await shared_grover.edit("/ws/@shared/alice/notes.md", "hello", "goodbye", user_id="bob")
        assert result.success is True

    async def test_exists_shared(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """exists returns True for shared path with permission."""
        await shared_grover.write("/ws/notes.md", "content", user_id="alice")
        await self._create_share(shared_grover, async_session, "/alice/notes.md", "bob", "read")

        result = await shared_grover.exists("/ws/@shared/alice/notes.md", user_id="bob")
        assert result.message == "exists"

    async def test_exists_shared_no_permission(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """exists returns False for shared path without permission."""
        await shared_grover.write("/ws/notes.md", "content", user_id="alice")
        result = await shared_grover.exists("/ws/@shared/alice/notes.md", user_id="bob")
        assert result.message != "exists"

    async def test_get_info_shared(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """get_info works for shared paths with permission."""
        await shared_grover.write("/ws/notes.md", "content", user_id="alice")
        await self._create_share(shared_grover, async_session, "/alice/notes.md", "bob", "read")

        info = await shared_grover.get_info("/ws/@shared/alice/notes.md", user_id="bob")
        assert info.success

    async def test_get_info_shared_no_permission(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """get_info returns success=False for shared path without permission."""
        await shared_grover.write("/ws/notes.md", "content", user_id="alice")
        info = await shared_grover.get_info("/ws/@shared/alice/notes.md", user_id="bob")
        assert not info.success

    async def test_directory_share_grants_children(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Share on /alice/projects grants read to /alice/projects/docs/file.md."""
        await shared_grover.write("/ws/projects/docs/file.md", "content", user_id="alice")
        await self._create_share(shared_grover, async_session, "/alice/projects", "bob", "read")

        result = await shared_grover.read("/ws/@shared/alice/projects/docs/file.md", user_id="bob")
        assert result.success is True
        assert result.file.content == "content"


# ---------------------------------------------------------------------------
# Integration: @shared list_dir virtual directories
# ---------------------------------------------------------------------------


class TestSharedListDir:
    async def _create_share(
        self,
        grover: GroverAsync,
        async_session: AsyncSession,
        path: str,
        grantee_id: str,
        permission: str = "read",
        granted_by: str = "alice",
    ) -> None:
        mount = grover._ctx.registry.get_mount("/ws")
        assert mount is not None
        backend = mount.filesystem
        assert isinstance(backend, UserScopedFileSystem)
        assert backend._share_model is not None
        await backend._create_share(async_session, path, grantee_id, permission, granted_by)
        await async_session.commit()

    async def test_list_dir_shared_root(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """/@shared lists distinct owners who shared with user."""
        await shared_grover.write("/ws/a.md", "a", user_id="alice")
        await shared_grover.write("/ws/b.md", "b", user_id="charlie")
        await self._create_share(shared_grover, async_session, "/alice/a.md", "bob", "read")
        await self._create_share(
            shared_grover,
            async_session,
            "/charlie/b.md",
            "bob",
            "read",
            granted_by="charlie",
        )

        result = await shared_grover.list_dir("/ws/@shared", user_id="bob")
        assert result.success is True
        names = {p.rsplit("/", 1)[-1] for p in result.paths}
        assert "alice" in names
        assert "charlie" in names
        assert set(result.paths) == set(_directories(result))

    async def test_list_dir_shared_owner(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """/@shared/{owner} lists that owner's shared content."""
        await shared_grover.write("/ws/notes.md", "content", user_id="alice")
        await shared_grover.write("/ws/readme.md", "readme", user_id="alice")
        # Share the alice root dir so bob can list everything
        await self._create_share(shared_grover, async_session, "/alice", "bob", "read")

        result = await shared_grover.list_dir("/ws/@shared/alice", user_id="bob")
        assert result.success is True
        names = {p.rsplit("/", 1)[-1] for p in result.paths}
        assert "notes.md" in names
        assert "readme.md" in names

    async def test_list_dir_shared_no_sharing_configured(self, auth_grover: GroverAsync):
        """@shared list_dir with no SharingService returns empty."""
        result = await auth_grover.list_dir("/ws/@shared", user_id="alice")
        assert result.success is True
        assert len(result) == 0

    async def test_list_dir_shared_owner_no_permission(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """/@shared/{owner} without share raises PermissionError."""
        await shared_grover.write("/ws/notes.md", "content", user_id="alice")
        with pytest.raises(PermissionError, match="Access denied"):
            await shared_grover.list_dir("/ws/@shared/alice", user_id="bob")

    async def test_list_dir_shared_owner_file_shares(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """FileModel-level shares show just those files when listing owner dir."""
        await shared_grover.write("/ws/doc1.md", "doc1", user_id="alice")
        await shared_grover.write("/ws/doc2.md", "doc2", user_id="alice")
        await shared_grover.write("/ws/secret.md", "secret", user_id="alice")
        await self._create_share(shared_grover, async_session, "/alice/doc1.md", "bob", "read")
        await self._create_share(shared_grover, async_session, "/alice/doc2.md", "bob", "read")

        result = await shared_grover.list_dir("/ws/@shared/alice", user_id="bob")
        assert result.success is True
        names = {p.rsplit("/", 1)[-1] for p in result.paths}
        assert names == {"doc1.md", "doc2.md"}
        # secret.md should NOT appear
        assert "secret.md" not in names

    async def test_list_dir_shared_owner_mixed_shares(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Directory share lists everything; file shares outside that dir also appear."""
        await shared_grover.write("/ws/projects/a.py", "a", user_id="alice")
        await shared_grover.write("/ws/projects/b.py", "b", user_id="alice")
        await shared_grover.write("/ws/readme.md", "readme", user_id="alice")
        # Directory share on /alice/projects gives full listing at that level
        await self._create_share(shared_grover, async_session, "/alice/projects", "bob", "read")
        # FileModel share on readme
        await self._create_share(shared_grover, async_session, "/alice/readme.md", "bob", "read")

        # At the /alice level, bob should see both projects/ dir and readme.md
        result = await shared_grover.list_dir("/ws/@shared/alice", user_id="bob")
        assert result.success is True
        names = {p.rsplit("/", 1)[-1] for p in result.paths}
        assert "projects" in names
        assert "readme.md" in names

    async def test_list_dir_shared_deep_navigation(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Deep file share shows intermediate dirs at each level."""
        await shared_grover.write("/ws/deep/nested/file.md", "deep content", user_id="alice")
        await self._create_share(shared_grover, async_session, "/alice/deep/nested/file.md", "bob", "read")

        # Level 1: /@shared/alice -> shows "deep/"
        result = await shared_grover.list_dir("/ws/@shared/alice", user_id="bob")
        assert result.success is True
        names = {p.rsplit("/", 1)[-1] for p in result.paths}
        assert names == {"deep"}
        assert len(_directories(result)) == 1

        # Level 2: /@shared/alice/deep -> shows "nested/"
        result = await shared_grover.list_dir("/ws/@shared/alice/deep", user_id="bob")
        assert result.success is True
        names = {p.rsplit("/", 1)[-1] for p in result.paths}
        assert names == {"nested"}
        assert len(_directories(result)) == 1

        # Level 3: /@shared/alice/deep/nested -> shows "file.md"
        result = await shared_grover.list_dir("/ws/@shared/alice/deep/nested", user_id="bob")
        assert result.success is True
        names = {p.rsplit("/", 1)[-1] for p in result.paths}
        assert names == {"file.md"}
        assert len(_files_only(result)) == 1

    async def test_list_dir_shared_directory_share_unchanged(
        self, shared_grover: GroverAsync, async_session: AsyncSession
    ):
        """Existing directory share behavior is preserved (fast path)."""
        await shared_grover.write("/ws/notes.md", "content", user_id="alice")
        await shared_grover.write("/ws/readme.md", "readme", user_id="alice")
        # Share the entire alice root
        await self._create_share(shared_grover, async_session, "/alice", "bob", "read")

        result = await shared_grover.list_dir("/ws/@shared/alice", user_id="bob")
        assert result.success is True
        names = {p.rsplit("/", 1)[-1] for p in result.paths}
        assert "notes.md" in names
        assert "readme.md" in names

    async def test_list_dir_shared_no_shares_still_raises(
        self, shared_grover: GroverAsync, async_session: AsyncSession
    ):
        """No shares at all still raises PermissionError."""
        await shared_grover.write("/ws/notes.md", "content", user_id="alice")
        with pytest.raises(PermissionError, match="Access denied"):
            await shared_grover.list_dir("/ws/@shared/alice", user_id="bob")


# ---------------------------------------------------------------------------
# Integration: move/copy via @shared paths
# ---------------------------------------------------------------------------


class TestSharedMoveAndCopy:
    async def _create_share(
        self,
        grover: GroverAsync,
        async_session: AsyncSession,
        path: str,
        grantee_id: str,
        permission: str = "read",
        granted_by: str = "alice",
    ) -> None:
        mount = grover._ctx.registry.get_mount("/ws")
        assert mount is not None
        backend = mount.filesystem
        assert isinstance(backend, UserScopedFileSystem)
        assert backend._share_model is not None
        await backend._create_share(async_session, path, grantee_id, permission, granted_by)
        await async_session.commit()

    async def test_copy_shared_file_with_read_perm(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Bob can copy alice's file to his own space with read share."""
        await shared_grover.write("/ws/notes.md", "alice's content", user_id="alice")
        await self._create_share(shared_grover, async_session, "/alice/notes.md", "bob", "read")

        result = await shared_grover.copy("/ws/@shared/alice/notes.md", "/ws/my_copy.md", user_id="bob")
        assert result.success is True
        read_result = await shared_grover.read("/ws/my_copy.md", user_id="bob")
        assert read_result.file.content == "alice's content"

    async def test_copy_shared_file_no_permission(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Bob cannot copy alice's file without a share."""
        await shared_grover.write("/ws/notes.md", "alice's content", user_id="alice")
        result = await shared_grover.copy("/ws/@shared/alice/notes.md", "/ws/stolen.md", user_id="bob")
        assert result.success is False
        assert "access denied" in result.message.lower()

    async def test_move_shared_file_no_permission(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Bob cannot move alice's file without a share."""
        await shared_grover.write("/ws/notes.md", "alice's content", user_id="alice")
        result = await shared_grover.move("/ws/@shared/alice/notes.md", "/ws/stolen.md", user_id="bob")
        assert result.success is False
        assert "access denied" in result.message.lower()

    async def test_move_shared_file_with_write_perm(self, shared_grover: GroverAsync, async_session: AsyncSession):
        """Bob can move alice's shared file with write permission on directory."""
        await shared_grover.write("/ws/old.md", "content", user_id="alice")
        # Directory-level share covers both source and destination
        await self._create_share(shared_grover, async_session, "/alice", "bob", "write")

        result = await shared_grover.move("/ws/@shared/alice/old.md", "/ws/@shared/alice/new.md", user_id="bob")
        assert result.success is True


# ---------------------------------------------------------------------------
# Trash scoping tests
# ---------------------------------------------------------------------------


class TestTrashScoping:
    """Trash operations scoped by owner_id on user-scoped mounts."""

    async def test_list_trash_scoped_by_owner(self, auth_grover: GroverAsync):
        """Each user only sees their own trashed files."""
        await auth_grover.write("/ws/a.md", "alice's file", user_id="alice")
        await auth_grover.write("/ws/b.md", "bob's file", user_id="bob")

        await auth_grover.delete("/ws/a.md", user_id="alice")
        await auth_grover.delete("/ws/b.md", user_id="bob")

        alice_trash = await auth_grover.list_trash(user_id="alice")
        assert alice_trash.success
        assert len(alice_trash) == 1
        assert any(p.endswith("a.md") for p in alice_trash.paths)

        bob_trash = await auth_grover.list_trash(user_id="bob")
        assert bob_trash.success
        assert len(bob_trash) == 1
        assert any(p.endswith("b.md") for p in bob_trash.paths)

    async def test_list_trash_regular_mount_shows_all(self, regular_grover: GroverAsync):
        """Non-user-scoped mount shows all trashed files regardless."""
        await regular_grover.write("/ws/a.md", "file a")
        await regular_grover.write("/ws/b.md", "file b")
        await regular_grover.delete("/ws/a.md")
        await regular_grover.delete("/ws/b.md")

        trash = await regular_grover.list_trash()
        assert trash.success
        assert len(trash) == 2

    async def test_restore_own_file(self, auth_grover: GroverAsync):
        """User can restore their own trashed file."""
        await auth_grover.write("/ws/mine.md", "my data", user_id="alice")
        await auth_grover.delete("/ws/mine.md", user_id="alice")

        result = await auth_grover.restore_from_trash("/ws/mine.md", user_id="alice")
        assert result.success is True

        r = await auth_grover.read("/ws/mine.md", user_id="alice")
        assert r.success
        assert r.file.content == "my data"

    async def test_restore_other_user_denied(self, auth_grover: GroverAsync):
        """User cannot restore another user's trashed file."""
        await auth_grover.write("/ws/secret.md", "alice's secret", user_id="alice")
        await auth_grover.delete("/ws/secret.md", user_id="alice")

        result = await auth_grover.restore_from_trash("/ws/secret.md", user_id="bob")
        assert result.success is False
        assert "not in trash" in result.message.lower()

    async def test_empty_trash_scoped(self, auth_grover: GroverAsync):
        """Emptying trash only deletes the requesting user's files."""
        await auth_grover.write("/ws/a.md", "alice", user_id="alice")
        await auth_grover.write("/ws/b.md", "bob", user_id="bob")
        await auth_grover.delete("/ws/a.md", user_id="alice")
        await auth_grover.delete("/ws/b.md", user_id="bob")

        # Alice empties her trash
        result = await auth_grover.empty_trash(user_id="alice")
        assert result.success
        assert "1" in result.message

        # Bob's trash still has his file
        bob_trash = await auth_grover.list_trash(user_id="bob")
        assert bob_trash.success
        assert len(bob_trash) == 1
        assert any(p.endswith("b.md") for p in bob_trash.paths)

        # Alice's trash is now empty
        alice_trash = await auth_grover.list_trash(user_id="alice")
        assert alice_trash.success
        assert len(alice_trash) == 0

    async def test_empty_trash_regular_mount_deletes_all(self, regular_grover: GroverAsync):
        """Non-user-scoped mount empties all trash."""
        await regular_grover.write("/ws/a.md", "file a")
        await regular_grover.write("/ws/b.md", "file b")
        await regular_grover.delete("/ws/a.md")
        await regular_grover.delete("/ws/b.md")

        result = await regular_grover.empty_trash()
        assert result.success

        trash = await regular_grover.list_trash()
        assert len(trash) == 0
