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

import pytest
from tests.helpers import FakeMCP, fake_rule, fake_rulebook, fake_topic


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


@pytest.mark.asyncio
async def test_list_rulebooks_wraps_in_dict():
    rows = [fake_rulebook(id=1, title="t"), fake_rulebook(id=2, title="t")]
    with patch(
        "scribe.mcp.tools.rulebooks.rulebooks_svc.list_rulebooks",
        AsyncMock(return_value=rows),
    ):
        from scribe.mcp.tools.rulebooks import list_rulebooks
        out = await list_rulebooks()
    assert len(out["rulebooks"]) == 2


@pytest.mark.asyncio
async def test_get_rulebook_includes_topics():
    rb = fake_rulebook(id=1, title="t")
    topics = [fake_topic(id=10, title="git"), fake_topic(id=11, title="git")]
    with patch(
        "scribe.mcp.tools.rulebooks.rulebooks_svc.get_rulebook",
        AsyncMock(return_value=rb),
    ), patch(
        "scribe.mcp.tools.rulebooks.rulebooks_svc.list_topics",
        AsyncMock(return_value=topics),
    ):
        from scribe.mcp.tools.rulebooks import get_rulebook
        out = await get_rulebook(rulebook_id=1)
    assert out["id"] == 1
    assert len(out["topics"]) == 2


@pytest.mark.asyncio
async def test_get_rulebook_raises_when_not_found():
    with patch(
        "scribe.mcp.tools.rulebooks.rulebooks_svc.get_rulebook",
        AsyncMock(return_value=None),
    ):
        from scribe.mcp.tools.rulebooks import get_rulebook
        with pytest.raises(ValueError, match="rulebook 999 not found"):
            await get_rulebook(rulebook_id=999)


@pytest.mark.asyncio
async def test_create_rule_passes_required_fields():
    rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
    mock = AsyncMock(return_value=rule)
    with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_rule", mock):
        from scribe.mcp.tools.rulebooks import create_rule
        await create_rule(
            topic_id=10, title="dev is home", statement="Work directly on dev",
        )
    kwargs = mock.call_args.kwargs
    assert kwargs["user_id"] == 7
    assert kwargs["topic_id"] == 10
    assert kwargs["statement"] == "Work directly on dev"


@pytest.mark.asyncio
async def test_create_rule_blocked_by_duplicate_gate():
    from scribe.services.dedup import DuplicateMatch
    dup = DuplicateMatch(id=47, title="dev is home", similarity=1.0, reason="title")
    create_mock = AsyncMock()
    with patch("scribe.mcp.tools.rulebooks.dedup_svc.find_duplicate_rule",
               AsyncMock(return_value=dup)), \
         patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_rule", create_mock):
        from scribe.mcp.tools.rulebooks import create_rule
        out = await create_rule(topic_id=10, title="dev is home", statement="x")
    assert out["duplicate"] is True
    assert out["existing_id"] == 47
    assert "update_rule" in out["message"]
    create_mock.assert_not_called()


@pytest.mark.asyncio
async def test_create_rule_force_bypasses_duplicate_gate():
    find_mock = AsyncMock()
    with patch("scribe.mcp.tools.rulebooks.dedup_svc.find_duplicate_rule", find_mock), \
         patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_rule",
               AsyncMock(return_value=fake_rule(id=5, title="r", statement="s", topic_id=10))):
        from scribe.mcp.tools.rulebooks import create_rule
        out = await create_rule(topic_id=10, title="dev is home", statement="x", force=True)
    assert out["id"] == 5
    find_mock.assert_not_called()


@pytest.mark.asyncio
async def test_update_rule_only_sends_non_default_fields():
    rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
    mock = AsyncMock(return_value=rule)
    with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.update_rule", mock):
        from scribe.mcp.tools.rulebooks import update_rule
        await update_rule(rule_id=1, statement="new statement")
    args, kwargs = mock.call_args
    assert args == (1, 7)
    assert kwargs == {"statement": "new statement"}


@pytest.mark.asyncio
async def test_delete_rule_without_confirmed_returns_warning():
    """delete_rule with confirmed=False returns a preview, not an action."""
    rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
    with patch(
        "scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
        AsyncMock(return_value=rule),
    ), patch(
        "scribe.mcp.tools.rulebooks.rulebooks_svc.delete_rule",
        AsyncMock(),
    ) as mock_delete:
        from scribe.mcp.tools.rulebooks import delete_rule
        out = await delete_rule(rule_id=1, confirmed=False)
    assert out.get("confirmed_required") is True
    assert "confirmed=True" in out.get("warning", "")
    assert not mock_delete.called  # service NOT called


@pytest.mark.asyncio
async def test_delete_rule_with_confirmed_soft_deletes():
    rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
    mock_delete = AsyncMock(return_value="batch-1")
    with patch(
        "scribe.mcp.tools.rulebooks.rulebooks_svc.get_rule",
        AsyncMock(return_value=rule),
    ), patch(
        "scribe.mcp.tools.rulebooks.trash_svc.delete",
        mock_delete,
    ):
        from scribe.mcp.tools.rulebooks import delete_rule
        out = await delete_rule(rule_id=1, confirmed=True)
    assert out["deleted"] == 1
    assert out["deleted_batch_id"] == "batch-1"
    assert mock_delete.called


@pytest.mark.asyncio
async def test_subscribe_project_to_rulebook_calls_service():
    mock = AsyncMock()
    with patch(
        "scribe.mcp.tools.rulebooks.rulebooks_svc.subscribe_project", mock,
    ):
        from scribe.mcp.tools.rulebooks import subscribe_project_to_rulebook
        out = await subscribe_project_to_rulebook(project_id=3, rulebook_id=1)
    assert out["subscribed"] is True
    assert mock.called


@pytest.mark.asyncio
async def test_unsubscribe_project_from_rulebook_calls_service():
    mock = AsyncMock()
    with patch(
        "scribe.mcp.tools.rulebooks.rulebooks_svc.unsubscribe_project", mock,
    ):
        from scribe.mcp.tools.rulebooks import unsubscribe_project_from_rulebook
        out = await unsubscribe_project_from_rulebook(project_id=3, rulebook_id=1)
    assert out["subscribed"] is False
    assert mock.called


def test_register_attaches_all_sixteen_tools():
    """register(mcp) should call mcp.tool(name=...) for all 16 tools."""
    from scribe.mcp.tools.rulebooks import register
    mcp = FakeMCP()

    register(mcp)
    assert len(mcp.names) == 24   # +exclude/include_always_on_rulebook (milestone 297)
    # spot-check a few names
    assert "list_rulebooks" in mcp.names
    assert "create_rule" in mcp.names
    assert "subscribe_project_to_rulebook" in mcp.names
    assert "list_always_on_rules" in mcp.names
    # milestone 297: a project's opt-out of a whole always-on rulebook
    assert "exclude_always_on_rulebook" in mcp.names
    assert "include_always_on_rulebook" in mcp.names
    assert "create_project_rule" in mcp.names
    assert "suppress_rule_for_project" in mcp.names
    assert "unsuppress_rule_for_project" in mcp.names
    assert "suppress_topic_for_project" in mcp.names
    assert "unsuppress_topic_for_project" in mcp.names


@pytest.mark.asyncio
async def test_list_always_on_rules_returns_empty_when_no_always_on_rulebooks():
    with patch(
        "scribe.mcp.tools.rulebooks.rulebooks_svc.list_always_on_rules",
        AsyncMock(return_value=[]),
    ):
        from scribe.mcp.tools.rulebooks import list_always_on_rules
        out = await list_always_on_rules()
    assert out == {"rules": [], "total": 0}


@pytest.mark.asyncio
async def test_list_always_on_rules_projects_each_rule():
    rules = [fake_rule(id=100, title="r", statement="s", topic_id=10), fake_rule(id=101, title="r", statement="s", topic_id=10)]
    with patch(
        "scribe.mcp.tools.rulebooks.rulebooks_svc.list_always_on_rules",
        AsyncMock(return_value=rules),
    ):
        from scribe.mcp.tools.rulebooks import list_always_on_rules
        out = await list_always_on_rules()
    assert out["total"] == 2
    assert {r["id"] for r in out["rules"]} == {100, 101}
    assert all("topic_id" in r for r in out["rules"])


@pytest.mark.asyncio
async def test_update_rulebook_forwards_always_on_when_set():
    rb = fake_rulebook(id=1, title="t")
    mock = AsyncMock(return_value=rb)
    with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.update_rulebook", mock):
        from scribe.mcp.tools.rulebooks import update_rulebook
        await update_rulebook(rulebook_id=1, always_on=True)
    kwargs = mock.call_args.kwargs
    assert kwargs.get("always_on") is True
    assert "title" not in kwargs
    assert "description" not in kwargs


@pytest.mark.asyncio
async def test_update_rulebook_omits_always_on_when_none():
    rb = fake_rulebook(id=1, title="t")
    mock = AsyncMock(return_value=rb)
    with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.update_rulebook", mock):
        from scribe.mcp.tools.rulebooks import update_rulebook
        await update_rulebook(rulebook_id=1, title="new title")
    kwargs = mock.call_args.kwargs
    assert "always_on" not in kwargs
    assert kwargs["title"] == "new title"


@pytest.mark.asyncio
async def test_create_project_rule_passes_required_fields():
    rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
    mock = AsyncMock(return_value=rule)
    with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock):
        from scribe.mcp.tools.rulebooks import create_project_rule
        await create_project_rule(
            project_id=42,
            statement="Always run migrations through alembic, not raw SQL.",
            why="audit trail",
        )
    kwargs = mock.call_args.kwargs
    assert kwargs["user_id"] == 7
    assert kwargs["project_id"] == 42
    assert kwargs["statement"].startswith("Always run migrations")
    assert kwargs["why"] == "audit trail"


@pytest.mark.asyncio
async def test_create_project_rule_derives_title_from_statement():
    rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
    mock = AsyncMock(return_value=rule)
    with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock):
        from scribe.mcp.tools.rulebooks import create_project_rule
        await create_project_rule(
            project_id=42,
            statement="Avoid auto-generated docstrings. Reviewers find them noise.",
        )
    kwargs = mock.call_args.kwargs
    # Title should be derived from the first sentence, capped at 50 chars
    assert kwargs["title"] == "Avoid auto-generated docstrings"


@pytest.mark.asyncio
async def test_create_project_rule_uses_explicit_title_when_given():
    rule = fake_rule(id=100, title="r", statement="s", topic_id=10)
    mock = AsyncMock(return_value=rule)
    with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.create_project_rule", mock):
        from scribe.mcp.tools.rulebooks import create_project_rule
        await create_project_rule(
            project_id=42,
            statement="anything",
            title="no auto-docstrings",
        )
    kwargs = mock.call_args.kwargs
    assert kwargs["title"] == "no auto-docstrings"


@pytest.mark.asyncio
async def test_suppress_rule_for_project_passes_through():
    mock = AsyncMock(return_value=None)
    with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.suppress_rule_for_project", mock):
        from scribe.mcp.tools.rulebooks import suppress_rule_for_project
        out = await suppress_rule_for_project(project_id=3, rule_id=17)
    kwargs = mock.call_args.kwargs
    assert kwargs == {"project_id": 3, "rule_id": 17, "user_id": 7}
    assert out == {"project_id": 3, "rule_id": 17, "suppressed": True}


@pytest.mark.asyncio
async def test_unsuppress_rule_for_project_passes_through():
    mock = AsyncMock(return_value=None)
    with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.unsuppress_rule_for_project", mock):
        from scribe.mcp.tools.rulebooks import unsuppress_rule_for_project
        out = await unsuppress_rule_for_project(project_id=3, rule_id=17)
    kwargs = mock.call_args.kwargs
    assert kwargs == {"project_id": 3, "rule_id": 17, "user_id": 7}
    assert out == {"project_id": 3, "rule_id": 17, "suppressed": False}


@pytest.mark.asyncio
async def test_suppress_topic_for_project_passes_through():
    mock = AsyncMock(return_value=None)
    with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.suppress_topic_for_project", mock):
        from scribe.mcp.tools.rulebooks import suppress_topic_for_project
        out = await suppress_topic_for_project(project_id=3, topic_id=22)
    kwargs = mock.call_args.kwargs
    assert kwargs == {"project_id": 3, "topic_id": 22, "user_id": 7}
    assert out == {"project_id": 3, "topic_id": 22, "suppressed": True}


@pytest.mark.asyncio
async def test_unsuppress_topic_for_project_passes_through():
    mock = AsyncMock(return_value=None)
    with patch("scribe.mcp.tools.rulebooks.rulebooks_svc.unsuppress_topic_for_project", mock):
        from scribe.mcp.tools.rulebooks import unsuppress_topic_for_project
        out = await unsuppress_topic_for_project(project_id=3, topic_id=22)
    kwargs = mock.call_args.kwargs
    assert kwargs == {"project_id": 3, "topic_id": 22, "user_id": 7}
    assert out == {"project_id": 3, "topic_id": 22, "suppressed": False}
