"""Editing a shared record: an editor grant is enough, a viewer grant is not.

The agent and web paths used to disagree — and worse, within the agent path
`delete_snippet` honoured editor shares while `update_snippet` did not, so a
colleague's snippet could be destroyed through an agent but not improved. Both
now resolve the read scope and then require WRITE, which is what the sharing UI
already promises: viewer / editor / admin are distinct grants.

A record readable-but-not-writable raises PermissionError rather than reporting
"not found" — the caller can plainly open it, so not-found would be a lie that
sends them hunting for a missing id.
"""
from unittest.mock import AsyncMock, patch

import pytest
from tests.helpers import fake_snippet


def _snippet(id=1, owner=9):
    return fake_snippet(
        id=id, user_id=owner, title="formatDuration — humanize a millisecond count",
        body="```ts\nexport const f = 1\n```\n", tags=["ts", "snippet"],
    )


@pytest.mark.asyncio
async def test_editor_share_can_update_and_writes_as_the_owner():
    """The underlying note update is owner-scoped, so a shared editor's own id
    would match nothing — the authorised write has to be performed as the owner."""
    from scribe.services import snippets as svc
    note = _snippet(owner=9)
    updated = _snippet(owner=9)
    with patch.object(svc, "get_snippet", AsyncMock(return_value=note)), \
         patch("scribe.services.access.can_write_note", AsyncMock(return_value=True)), \
         patch.object(svc.notes_svc, "update_note",
                      AsyncMock(return_value=updated)) as mock_update:
        got = await svc.update_snippet(7, 1, name="formatDuration")

    assert got is updated
    # Positional user_id is the OWNER (9), not the caller (7).
    assert mock_update.await_args.args[0] == 9


@pytest.mark.asyncio
async def test_viewer_share_cannot_update():
    from scribe.services import snippets as svc
    with patch.object(svc, "get_snippet", AsyncMock(return_value=_snippet())), \
         patch("scribe.services.access.can_write_note", AsyncMock(return_value=False)), \
         patch.object(svc.notes_svc, "update_note", AsyncMock()) as mock_update:
        with pytest.raises(PermissionError, match="read-only"):
            await svc.update_snippet(7, 1, name="nope")
    mock_update.assert_not_awaited()


@pytest.mark.asyncio
async def test_viewer_share_cannot_delete():
    """delete_snippet returns False rather than raising — its contract is boolean
    — but a viewer must not be able to bin someone else's record."""
    from scribe.services import snippets as svc
    with patch.object(svc, "get_snippet", AsyncMock(return_value=_snippet())), \
         patch("scribe.services.access.can_write_note", AsyncMock(return_value=False)), \
         patch("scribe.services.trash.delete", AsyncMock()) as mock_trash:
        assert await svc.delete_snippet(7, 1) is False
    mock_trash.assert_not_awaited()


@pytest.mark.asyncio
async def test_unreadable_record_is_not_found_not_forbidden():
    """A record the caller can't see at all must NOT be distinguishable from one
    that doesn't exist — otherwise the error itself confirms it exists."""
    from scribe.services import snippets as svc
    with patch.object(svc, "get_snippet", AsyncMock(return_value=None)):
        assert await svc.update_snippet(7, 404, name="x") is None
        assert await svc.delete_snippet(7, 404) is False


@pytest.mark.asyncio
async def test_merge_requires_write_on_target():
    from scribe.services import snippets as svc
    with patch.object(svc, "get_snippet", AsyncMock(return_value=_snippet())), \
         patch("scribe.services.access.can_write_note", AsyncMock(return_value=False)):
        with pytest.raises(PermissionError, match="read-only"):
            await svc.merge_snippets(7, 1, [2])


@pytest.mark.asyncio
async def test_merge_skips_sources_owned_by_someone_else():
    """Cross-owner merge stays out of scope (#231): a source belonging to a
    different owner than the target is skipped, not silently folded in and
    trashed."""
    from scribe.services import snippets as svc
    target = _snippet(id=1, owner=9)
    same_owner = _snippet(id=2, owner=9)
    other_owner = _snippet(id=3, owner=42)

    async def fake_get(_uid, sid):
        return {1: target, 2: same_owner, 3: other_owner}.get(sid)

    with patch.object(svc, "get_snippet", AsyncMock(side_effect=fake_get)), \
         patch("scribe.services.access.can_write_note", AsyncMock(return_value=True)), \
         patch.object(svc.notes_svc, "update_note", AsyncMock(return_value=target)), \
         patch("scribe.services.trash.delete", AsyncMock(return_value=object())):
        _note, merged_ids = await svc.merge_snippets(7, 1, [2, 3])

    assert merged_ids == [2]
