"""MCP design-system tools — the sentinel translations, mostly.

The tools are thin wrappers, so the only logic worth testing is where the MCP
calling convention meets the service's: an agent cannot omit an argument, so
"leave unchanged", "clear" and "set" have to be encoded in the value. Getting
that mapping wrong is silent — the call succeeds and changes the wrong thing.
"""
from unittest.mock import AsyncMock, patch

import pytest

from scribe.services.design_systems import DesignSystemCycle
from tests.helpers import design_token_stub, fake_record


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


def _fake_design_system():
    return fake_record(id=1, title="FabledSword", parent_id=None)


def _fake_token():
    return fake_record(id=9, name="--fs-obsidian")


# --- create -----------------------------------------------------------------

@pytest.mark.asyncio
async def test_creating_without_a_parent_passes_none_not_zero():
    """0 is the "omitted" sentinel, and it must not reach the service as a
    system id — there is no system 0, so the create would fail an ACL check for
    a record that cannot exist."""
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.create_design_system = AsyncMock(return_value=_fake_design_system())
        from scribe.mcp.tools.design_systems import create_design_system
        await create_design_system(title="FabledSword")
    assert svc.create_design_system.await_args.kwargs["parent_id"] is None


@pytest.mark.asyncio
async def test_creating_with_a_parent_passes_it_through():
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.create_design_system = AsyncMock(return_value=_fake_design_system())
        from scribe.mcp.tools.design_systems import create_design_system
        await create_design_system(title="Scribe", parent_id=4)
    assert svc.create_design_system.await_args.kwargs["parent_id"] == 4


@pytest.mark.asyncio
async def test_create_raises_when_the_parent_is_not_writable():
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.create_design_system = AsyncMock(return_value=None)
        from scribe.mcp.tools.design_systems import create_design_system
        with pytest.raises(ValueError):
            await create_design_system(title="Scribe", parent_id=4)


# --- the three-state parent -------------------------------------------------

@pytest.mark.asyncio
async def test_update_with_parent_id_zero_leaves_the_parent_alone():
    """The common case — renaming a system must not silently re-root it."""
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.update_design_system = AsyncMock(return_value=_fake_design_system())
        from scribe.mcp.tools.design_systems import update_design_system
        await update_design_system(design_system_id=1, title="Renamed")
    fields = svc.update_design_system.await_args.kwargs
    assert "parent_id" not in fields
    assert fields["title"] == "Renamed"


@pytest.mark.asyncio
async def test_update_with_parent_id_minus_one_clears_it():
    """-1 means "make this a family system". It has to arrive at the service as
    None, which is the value the service reads as "become a root" — where
    omitting the key means "leave alone"."""
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.update_design_system = AsyncMock(return_value=_fake_design_system())
        from scribe.mcp.tools.design_systems import update_design_system
        await update_design_system(design_system_id=1, parent_id=-1)
    assert svc.update_design_system.await_args.kwargs["parent_id"] is None


@pytest.mark.asyncio
async def test_update_with_a_positive_parent_id_sets_it():
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.update_design_system = AsyncMock(return_value=_fake_design_system())
        from scribe.mcp.tools.design_systems import update_design_system
        await update_design_system(design_system_id=1, parent_id=4)
    assert svc.update_design_system.await_args.kwargs["parent_id"] == 4


@pytest.mark.asyncio
async def test_a_cycle_surfaces_as_a_usable_error_not_a_not_found():
    """The service raises so this layer can keep the two apart. An agent told
    "not found" would retry the same call; one told what the loop is can fix it."""
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.update_design_system = AsyncMock(
            side_effect=DesignSystemCycle("2 already inherits from 1")
        )
        from scribe.mcp.tools.design_systems import update_design_system
        with pytest.raises(ValueError, match="already inherits"):
            await update_design_system(design_system_id=1, parent_id=2)


# --- tokens -----------------------------------------------------------------

@pytest.mark.asyncio
async def test_update_token_treats_order_index_minus_one_as_unchanged():
    """0 is a VALID order_index, so it cannot double as the omitted sentinel —
    the same reason the systems tools use -1."""
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.update_token = AsyncMock(return_value=_fake_token())
        from scribe.mcp.tools.design_systems import update_design_token
        await update_design_token(token_id=9, purpose="page bg")
    fields = svc.update_token.await_args.kwargs
    assert "order_index" not in fields
    assert fields["purpose"] == "page bg"


@pytest.mark.asyncio
async def test_update_token_accepts_order_index_zero():
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.update_token = AsyncMock(return_value=_fake_token())
        from scribe.mcp.tools.design_systems import update_design_token
        await update_design_token(token_id=9, order_index=0)
    assert svc.update_token.await_args.kwargs["order_index"] == 0


@pytest.mark.asyncio
async def test_update_token_can_set_an_empty_value_map():
    """`value_by_mode={}` is meaningful — it strips every mode from a token. The
    guard is `is not None`, not truthiness, or that edit would be unreachable."""
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.update_token = AsyncMock(return_value=_fake_token())
        from scribe.mcp.tools.design_systems import update_design_token
        await update_design_token(token_id=9, value_by_mode={})
    assert svc.update_token.await_args.kwargs["value_by_mode"] == {}


# --- the project pointer ----------------------------------------------------

@pytest.mark.asyncio
async def test_clearing_a_projects_design_system_passes_none():
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.set_project_design_system = AsyncMock(return_value=True)
        from scribe.mcp.tools.design_systems import set_project_design_system
        result = await set_project_design_system(project_id=2, design_system_id=-1)
    assert svc.set_project_design_system.await_args.args[2] is None
    assert result["design_system_id"] is None


@pytest.mark.asyncio
async def test_resolve_returns_serialised_tokens_with_their_provenance():
    """The payload has to carry the shadowed entries, not just the winner —
    dropping them at the serialisation boundary would discard the one thing
    resolution was built to preserve."""
    from scribe.services.design_cascade import resolve_tokens


    resolved = resolve_tokens(
        2, {1: None, 2: 1},
        {1: [design_token_stub("--fs-accent", {"base": "#6b2118"})],
         2: [design_token_stub("--fs-accent", {"base": "#5b4a8a"})]},
    )
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.resolve_design_system = AsyncMock(return_value=resolved)
        from scribe.mcp.tools.design_systems import resolve_design_system
        payload = await resolve_design_system(design_system_id=2)

    token = payload["tokens"][0]
    assert token["value_by_mode"] == {"base": "#5b4a8a"}
    assert token["origin_by_mode"] == {"base": 2}
    assert token["contributions"]["base"] == [
        {"system_id": 2, "value": "#5b4a8a"},
        {"system_id": 1, "value": "#6b2118"},
    ]


@pytest.mark.asyncio
async def test_update_token_can_clear_supersedes_with_an_empty_list():
    """`[]` means "this token replaces nothing after all" — a real edit. Guarded
    on `is not None` so it isn't swallowed as "unchanged", the same trap #2077
    recorded for update_snippet."""
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.update_token = AsyncMock(return_value=_fake_token())
        from scribe.mcp.tools.design_systems import update_design_token
        await update_design_token(token_id=9, supersedes=[])
    assert svc.update_token.await_args.kwargs["supersedes"] == []


@pytest.mark.asyncio
async def test_update_token_leaves_supersedes_alone_when_omitted():
    with patch("scribe.mcp.tools.design_systems.ds_svc") as svc:
        svc.update_token = AsyncMock(return_value=_fake_token())
        from scribe.mcp.tools.design_systems import update_design_token
        await update_design_token(token_id=9, purpose="text on action surfaces")
    assert "supersedes" not in svc.update_token.await_args.kwargs
