"""Structural tests for the snippets blueprint — registration + handler/service
contracts. Full HTTP integration needs a live DB + auth the unit env lacks."""
import inspect


def test_snippets_blueprint_registered():
    from scribe.routes.snippets import snippets_bp
    assert snippets_bp.name == "snippets"
    assert snippets_bp.url_prefix == "/api/snippets"


def test_snippets_blueprint_registered_in_app():
    from scribe.app import create_app
    app = create_app()
    assert "snippets" in app.blueprints


def test_snippet_handlers_callable():
    from scribe.routes import snippets as routes
    for name in (
        "list_snippets_route", "create_snippet_route", "get_snippet_route",
        "update_snippet_route", "delete_snippet_route", "merge_snippet_route",
        "verify_snippet_route", "duplicate_snippets_route",
        "unmerge_snippet_route",
    ):
        assert callable(getattr(routes, name))


def test_service_functions_take_user_id():
    """Routes must call snippet services with user_id — verify the contract."""
    from scribe.services import snippets as svc
    for fn_name in (
        "create_snippet", "list_snippets", "get_snippet", "update_snippet",
        "delete_snippet", "merge_snippets", "record_verification",
        "unmerge_snippet",
    ):
        fn = getattr(svc, fn_name)
        assert callable(fn)
        assert "user_id" in inspect.signature(fn).parameters


def test_agent_and_web_surfaces_stay_at_parity():
    """The MCP tools and the REST routes are two callers of one service; a
    capability on one has to exist on the other (rule #33). This guard exists
    because they drifted apart once: MCP had no delete or `locations`, and the
    web side had no `system_ids` and no near-duplicate gate."""
    from scribe.mcp.tools import snippets as tools
    from scribe.routes import snippets as routes

    # Every write verb the web surface offers, the agent surface offers too.
    for verb in ("create", "get", "list", "update", "delete", "merge", "verify",
                 "unmerge"):
        assert callable(getattr(tools, f"{verb}_snippets", None) or
                        getattr(tools, f"{verb}_snippet", None)), f"MCP lacks {verb}"

    # Multi-location records are reachable from both.
    assert "locations" in inspect.signature(tools.create_snippet).parameters
    assert "locations" in inspect.signature(tools.update_snippet).parameters
    assert "locations" in inspect.getsource(routes.create_snippet_route)
    assert "locations" in inspect.getsource(routes.update_snippet_route)

    # System association and the duplicate gate reach both.
    assert "system_ids" in inspect.signature(tools.create_snippet).parameters
    for route in (routes.create_snippet_route, routes.update_snippet_route):
        assert "system_ids" in inspect.getsource(route)
    assert "find_duplicate_note" in inspect.getsource(routes.create_snippet_route)

    # The drift check (#2086) reaches both: recording a verdict, and filtering
    # the list by one. A verdict an agent records has to be visible to the human
    # looking at the same corpus, or the two surfaces disagree about what's rotten.
    assert "verification" in inspect.signature(tools.list_snippets).parameters
    assert "verification" in inspect.getsource(routes.list_snippets_route)

    # The near-duplicate report (#2088) reaches both. The web side can only hand
    # a group to the merge flow if it can get the groups in the first place.
    assert callable(getattr(tools, "find_duplicate_snippets", None))
    assert callable(getattr(routes, "duplicate_snippets_route", None))


def test_project_scoping_reaches_every_caller():
    """A snippet search has to be narrowable to one project from both surfaces."""
    from scribe.mcp.tools import snippets as tools
    from scribe.routes import snippets as routes
    from scribe.services import snippets as svc
    assert "project_id" in inspect.signature(svc.list_snippets).parameters
    assert "project_id" in inspect.signature(tools.list_snippets).parameters
    assert "project_id" in inspect.getsource(routes.list_snippets_route)


def test_location_lookup_reaches_every_caller():
    """The reverse lookup — "what already lives here?" — has to be askable from
    both surfaces, or an agent and a human get different answers about the same
    file (rule #33)."""
    from scribe.mcp.tools import snippets as tools
    from scribe.routes import snippets as routes
    from scribe.services import snippets as svc
    for key in ("repo", "path", "symbol"):
        assert key in inspect.signature(svc.list_snippets).parameters
        assert key in inspect.signature(tools.list_snippets).parameters
        assert f'request.args.get("{key}"' in inspect.getsource(routes)


def test_update_field_map_matches_service_kwargs():
    """Every field the PATCH route forwards must be a real update_snippet kwarg
    (rule #33 interface-contract parity)."""
    from scribe.routes import snippets as routes
    from scribe.services import snippets as svc
    params = inspect.signature(svc.update_snippet).parameters
    for field in routes._STR_FIELDS:
        assert field in params, f"update_snippet has no '{field}' kwarg"
    assert "tags" in params
    assert "project_id" in params
