"""Tests for MCP snippet tools — patches the service layer."""
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
from tests.helpers import FakeMCP, fake_snippet


pytestmark = pytest.mark.usefixtures("_bind_user")


@pytest.mark.asyncio
async def test_create_snippet_requires_name_and_code():
    from scribe.mcp.tools.snippets import create_snippet
    with pytest.raises(ValueError):
        await create_snippet(name="", code="x")
    with pytest.raises(ValueError):
        await create_snippet(name="x", code="   ")


@pytest.mark.asyncio
async def test_create_snippet_records_and_returns_parsed():
    created = fake_snippet()
    with patch("scribe.services.dedup.find_duplicate_note", AsyncMock(return_value=None)), \
         patch("scribe.services.snippets.create_snippet",
               AsyncMock(return_value=created)) as mock_create:
        from scribe.mcp.tools.snippets import create_snippet
        out = await create_snippet(
            name="debounce", code="return 1", language="js",
            when_to_use="rate-limit a callback",
        )
    assert out["note_type"] == "snippet"
    assert out["snippet"]["name"] == "debounce"
    assert out["snippet"]["language"] == "js"
    assert mock_create.await_args.kwargs["name"] == "debounce"


@pytest.mark.asyncio
async def test_create_snippet_dedup_blocks_and_labels_snippet():
    dup = MagicMock(id=99)
    with patch("scribe.services.dedup.find_duplicate_note", AsyncMock(return_value=dup)), \
         patch("scribe.services.dedup.duplicate_response",
               MagicMock(return_value={"duplicate": True, "existing_id": 99})) as mock_resp, \
         patch("scribe.services.snippets.create_snippet", AsyncMock()) as mock_create:
        from scribe.mcp.tools.snippets import create_snippet
        out = await create_snippet(name="debounce", code="return 1")
    assert out["duplicate"] is True
    mock_create.assert_not_awaited()
    assert mock_resp.call_args.args[1] == "snippet"


@pytest.mark.asyncio
async def test_get_snippet_not_found_raises():
    with patch("scribe.services.snippets.get_snippet", AsyncMock(return_value=None)):
        from scribe.mcp.tools.snippets import get_snippet
        with pytest.raises(ValueError):
            await get_snippet(123)


@pytest.mark.asyncio
async def test_update_snippet_missing_raises():
    with patch("scribe.services.snippets.update_snippet", AsyncMock(return_value=None)):
        from scribe.mcp.tools.snippets import update_snippet
        with pytest.raises(ValueError):
            await update_snippet(123, name="x")


@pytest.mark.asyncio
async def test_update_snippet_empty_string_clears_a_field():
    # An omitted field must stay None ("leave alone"), but an explicit empty
    # string has to reach the service as "" so a stale field can be removed.
    updated = fake_snippet()
    with patch("scribe.services.snippets.update_snippet",
               AsyncMock(return_value=updated)) as mock_update:
        from scribe.mcp.tools.snippets import update_snippet
        await update_snippet(1, signature="")
    kwargs = mock_update.await_args.kwargs
    assert kwargs["signature"] == ""      # cleared
    assert kwargs["name"] is None         # untouched


@pytest.mark.asyncio
async def test_update_snippet_project_id_conventions():
    from scribe.services import snippets as snippets_svc
    updated = fake_snippet()
    cases = {0: snippets_svc.UNSET, -1: None, 5: 5}
    for given, expected in cases.items():
        with patch("scribe.services.snippets.update_snippet",
                   AsyncMock(return_value=updated)) as mock_update:
            from scribe.mcp.tools.snippets import update_snippet
            await update_snippet(1, project_id=given)
        assert mock_update.await_args.kwargs["project_id"] is expected


@pytest.mark.asyncio
async def test_create_and_update_pass_locations_through():
    locs = [{"repo": "a", "path": "a.py", "symbol": "f"},
            {"repo": "b", "path": "b.py", "symbol": "g"}]
    created = fake_snippet()
    with patch("scribe.services.dedup.find_duplicate_note", AsyncMock(return_value=None)), \
         patch("scribe.services.snippets.create_snippet",
               AsyncMock(return_value=created)) as mock_create:
        from scribe.mcp.tools.snippets import create_snippet
        await create_snippet(name="f", code="x", locations=locs)
    assert mock_create.await_args.kwargs["locations"] == locs

    with patch("scribe.services.snippets.update_snippet",
               AsyncMock(return_value=created)) as mock_update:
        from scribe.mcp.tools.snippets import update_snippet
        await update_snippet(1, locations=locs)
    assert mock_update.await_args.kwargs["locations"] == locs


@pytest.mark.asyncio
async def test_update_snippet_read_only_share_says_why():
    """A viewer grant must be refused with the REAL reason. "Not found" would be
    a lie about a record the caller can plainly open, and would send an agent
    hunting for a missing id instead of recording its own version."""
    with patch("scribe.services.snippets.update_snippet",
               AsyncMock(side_effect=PermissionError(
                   "snippet 1 is shared with you read-only — ask its owner for "
                   "edit access, or record your own version"))):
        from scribe.mcp.tools.snippets import update_snippet
        with pytest.raises(ValueError, match="read-only"):
            await update_snippet(1, name="x")


@pytest.mark.asyncio
async def test_merge_snippets_read_only_target_says_why():
    with patch("scribe.services.snippets.merge_snippets",
               AsyncMock(side_effect=PermissionError(
                   "snippet 1 is shared with you read-only — you can't merge "
                   "into a record you can't edit"))):
        from scribe.mcp.tools.snippets import merge_snippets
        with pytest.raises(ValueError, match="read-only"):
            await merge_snippets(target_id=1, source_ids=[2])


@pytest.mark.asyncio
async def test_delete_snippet_retires_or_raises():
    from scribe.mcp.tools.snippets import delete_snippet
    with patch("scribe.services.snippets.delete_snippet", AsyncMock(return_value=True)):
        assert await delete_snippet(1) == {"deleted": True, "id": 1}
    with patch("scribe.services.snippets.delete_snippet", AsyncMock(return_value=False)):
        with pytest.raises(ValueError):
            await delete_snippet(404)


@pytest.mark.asyncio
async def test_list_snippets_defaults_to_every_project():
    with patch("scribe.services.snippets.list_snippets",
               AsyncMock(return_value=([], 0))) as mock_list:
        from scribe.mcp.tools.snippets import list_snippets
        await list_snippets(q="debounce")
        assert mock_list.await_args.kwargs["project_id"] is None
        await list_snippets(q="debounce", project_id=3)
        assert mock_list.await_args.kwargs["project_id"] == 3


@pytest.mark.asyncio
async def test_merge_snippets_requires_a_source():
    from scribe.mcp.tools.snippets import merge_snippets
    with pytest.raises(ValueError):
        await merge_snippets(target_id=1, source_ids=[])
    with pytest.raises(ValueError):
        await merge_snippets(target_id=1, source_ids=[1])  # only self → nothing to merge


@pytest.mark.asyncio
async def test_merge_snippets_returns_survivor_and_merged_ids():
    survivor = fake_snippet()
    with patch("scribe.services.snippets.merge_snippets",
               AsyncMock(return_value=(survivor, [2, 3]))) as mock_merge:
        from scribe.mcp.tools.snippets import merge_snippets
        out = await merge_snippets(target_id=1, source_ids=[2, 3, 1])
    assert out["merged_ids"] == [2, 3]
    assert out["snippet"]["name"] == "debounce"
    # target_id passed through; self-id filtered out of the source list.
    assert mock_merge.await_args.args[1] == 1
    assert mock_merge.await_args.args[2] == [2, 3]


@pytest.mark.asyncio
async def test_merge_snippets_not_found_raises():
    with patch("scribe.services.snippets.merge_snippets", AsyncMock(return_value=None)):
        from scribe.mcp.tools.snippets import merge_snippets
        with pytest.raises(ValueError):
            await merge_snippets(target_id=1, source_ids=[2])


def test_register_attaches_all_tools():
    from scribe.mcp.tools import snippets
    mcp = FakeMCP()

    snippets.register(mcp)
    assert set(mcp.names) == {
        "list_snippets", "create_snippet", "get_snippet", "update_snippet",
        "delete_snippet", "merge_snippets", "verify_snippet",
        "find_duplicate_snippets", "unmerge_snippet",
    }
