"""Milestone 297 step 2 — always-on exclusions reach every rule surface.

The SQL is the integration lane's; here the contracts: rules_payload carries
the seventh key, list_always_on_rules takes project_id, the session-start
block names the excluded rulebooks, and the MCP tools mount.
"""
from unittest.mock import AsyncMock, patch

import pytest

from scribe.services.rulebooks import rules_payload


def test_rules_payload_carries_excluded_always_on_as_the_seventh_key():
    out = rules_payload({
        "rules": [], "truncated": False, "subscribed_rulebooks": [],
        "excluded_always_on": [{"id": 1, "title": "Family"}],
    })
    assert set(out) == {
        "applicable_rules", "applicable_rules_truncated", "subscribed_rulebooks",
        "project_rules", "suppressed_rules", "suppressed_topics", "excluded_always_on",
    }
    assert out["excluded_always_on"] == [{"id": 1, "title": "Family"}]
    # An older applicable dict without the key still renders (empty list).
    assert rules_payload({"rules": [], "truncated": False, "subscribed_rulebooks": []})["excluded_always_on"] == []


def test_list_always_on_rules_service_and_tool_take_a_project_id():
    import inspect

    from scribe.mcp.tools import rulebooks as tools
    from scribe.services import rulebooks as svc
    assert "project_id" in inspect.signature(svc.list_always_on_rules).parameters
    assert "project_id" in inspect.signature(tools.list_always_on_rules).parameters


@pytest.mark.asyncio
async def test_session_context_names_the_excluded_always_on_rulebooks():
    from types import SimpleNamespace as NS

    from scribe.services.plugin_context import build_session_context
    rules = [NS(id=1, title="`dev` is home", topic_id=1, statement="x")]
    project = NS(id=9, title="Widget", goal="", design_system_id=None)
    with patch("scribe.services.plugin_context.rulebooks_svc.list_always_on_rules",
               AsyncMock(return_value=rules)) as lao, \
         patch("scribe.services.plugin_context.rulebooks_svc.excluded_always_on_rulebooks",
               AsyncMock(return_value=[{"id": 5, "title": "Design standards"}])), \
         patch("scribe.services.plugin_context._topic_titles", AsyncMock(return_value={1: "git"})), \
         patch("scribe.services.plugin_context.projects_svc.get_project", AsyncMock(return_value=project)), \
         patch("scribe.services.plugin_context.notes_svc.list_notes", AsyncMock(return_value=([], 0))), \
         patch("scribe.services.plugin_context.rulebooks_svc.get_applicable_rules",
               AsyncMock(return_value={"rules": [], "truncated": False, "subscribed_rulebooks": [],
                                       "project_rules": [], "suppressed_rules": [],
                                       "suppressed_topics": [], "excluded_always_on": []})):
        out = await build_session_context(user_id=7, project_id=9)
    # The always-on set was asked FOR THIS PROJECT, and the departure is named.
    assert lao.await_args.kwargs.get("project_id") == 9
    assert "Excluded for this project by its inception decision" in out["context"]
    assert "Design standards (#5)" in out["context"]


def test_exclusion_routes_are_registered():
    from scribe.app import create_app
    rules = {r.rule for r in create_app().url_map.iter_rules()}
    assert "/api/projects/<int:project_id>/exclusions/rulebooks/<int:rulebook_id>" in rules

