from __future__ import annotations

import asyncio

import pytest

import oh_my_linear.server as server


def _tool_names() -> set[str]:
    return set(server.mcp._tool_manager._tools.keys())


def _tool_description(name: str) -> str:
    return server.mcp._tool_manager._tools[name].description or ""


class _FakeRouter:
    def __init__(self) -> None:
        self.read_calls: list[tuple[str, dict[str, object]]] = []
        self.official_calls: list[tuple[str, dict[str, object]]] = []

    def call_read(self, name: str, args: dict[str, object]) -> dict[str, object]:
        self.read_calls.append((name, args))
        return {"path": "read", "name": name, "args": args}

    def call_official(self, name: str, args: dict[str, object]) -> dict[str, object]:
        self.official_calls.append((name, args))
        return {"path": "official", "name": name, "args": args}


def test_server_exports_main_and_mcp() -> None:
    assert callable(server.main)
    assert server.mcp is not None


def test_module_does_not_export_removed_ops_tools() -> None:
    assert not hasattr(server, "list_official_tools")
    assert not hasattr(server, "reconnect_official")
    assert not hasattr(server, "refresh_cache")
    assert not hasattr(server, "get_cache_health")
    assert not hasattr(server, "reauth")


def test_tool_surface_excludes_removed_ops_tools() -> None:
    tool_names = _tool_names()

    assert "call_linear_tool_raw" in tool_names
    assert "official_call_tool" not in tool_names
    assert "list_official_tools" not in tool_names
    assert "reconnect_official" not in tool_names
    assert "refresh_cache" not in tool_names
    assert "get_cache_health" not in tool_names
    assert "reauth" not in tool_names


def test_read_helper_raises_runtime_error_when_router_missing(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setattr(server, "_router", None)
    with pytest.raises(RuntimeError, match="server not initialized"):
        server._read("list_users")


def test_call_linear_tool_raw_raises_runtime_error_when_router_missing(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    monkeypatch.setattr(server, "_router", None)
    with pytest.raises(RuntimeError, match="server not initialized"):
        server.call_linear_tool_raw("create_issue", {})


def test_read_helper_forwards_to_router(monkeypatch: pytest.MonkeyPatch) -> None:
    fake_router = _FakeRouter()
    monkeypatch.setattr(server, "_router", fake_router)

    result = server._read("list_users", limit=5)

    assert result["path"] == "read"
    assert fake_router.read_calls == [("list_users", {"limit": 5})]


def test_call_linear_tool_raw_forwards_to_router(monkeypatch: pytest.MonkeyPatch) -> None:
    fake_router = _FakeRouter()
    monkeypatch.setattr(server, "_router", fake_router)

    result = server.call_linear_tool_raw("create_issue", {"title": "Test"})

    assert result["path"] == "official"
    assert fake_router.official_calls == [("create_issue", {"title": "Test"})]


def test_deprecated_official_call_tool_alias_forwards_to_router(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    fake_router = _FakeRouter()
    monkeypatch.setattr(server, "_router", fake_router)

    result = server.official_call_tool("create_issue", {"title": "Legacy"})

    assert result["path"] == "official"
    assert fake_router.official_calls == [("create_issue", {"title": "Legacy"})]


def test_compose_official_tool_surface_syncs_shared_descriptions_and_adds_passthroughs(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    fake_router = _FakeRouter()
    monkeypatch.setattr(server, "_router", fake_router)
    local_list_issues_tool = server.mcp._tool_manager._tools["list_issues"]
    original_list_issues_description = local_list_issues_tool.description
    original_list_issues_parameters = local_list_issues_tool.parameters
    local_only_tool = server.mcp._tool_manager._tools["list_project_updates"]
    original_local_only_description = local_only_tool.description

    create_issue_schema = {
        "type": "object",
        "properties": {
            "title": {"type": "string"},
            "description": {"type": "string"},
        },
        "required": ["title"],
    }

    class _FakeOfficial:
        def list_tools(self):
            return [
                {
                    "name": "create_issue",
                    "description": "Create a Linear issue.",
                    "inputSchema": create_issue_schema,
                },
                {
                    # Non-identifier parameter names should still be accepted and forwarded.
                    "name": "create_external_issue",
                    "description": "Create external issue.",
                    "inputSchema": {
                        "type": "object",
                        "properties": {
                            "team-id": {"type": "string"},
                            "title": {"type": "string"},
                        },
                        "required": ["team-id", "title"],
                    },
                },
                {
                    # Should be ignored because local read tool already owns this name.
                    "name": "list_issues",
                    "description": (
                        "List issues in the user's Linear workspace. "
                        'For my issues, use "me" as the assignee. '
                        'Use "null" for no assignee.'
                    ),
                    "inputSchema": {"type": "object"},
                },
            ]

    if "create_issue" in server.mcp._tool_manager._tools:
        server.mcp.remove_tool("create_issue")

    try:
        added = server._compose_official_tool_surface(_FakeOfficial())
        tool_names = _tool_names()

        assert "create_issue" in added
        assert "create_external_issue" in added
        assert "list_issues" not in added
        assert "create_issue" in tool_names
        assert "create_external_issue" in tool_names
        assert "list_issues" in tool_names
        assert "call_linear_tool_raw" in tool_names
        assert "official_call_tool" not in tool_names
        assert server.mcp._tool_manager._tools["list_issues"] is local_list_issues_tool
        assert _tool_description("list_issues") != (original_list_issues_description or "")
        assert (
            _tool_description("list_issues")
            == "List issues in the user's Linear workspace. "
            'For my issues, use "me" as the assignee. '
            'Use "null" for no assignee.'
        )
        assert local_list_issues_tool.parameters == original_list_issues_parameters
        assert _tool_description("list_project_updates") == original_local_only_description

        create_issue_tool = server.mcp._tool_manager._tools.get("create_issue")
        assert create_issue_tool is not None
        assert create_issue_tool.description == "Create a Linear issue."
        assert create_issue_tool.parameters == create_issue_schema

        asyncio.run(
            server.mcp.call_tool(
                "create_issue",
                {
                    "title": "Ship it",
                    "description": None,
                    "unexpected-extra": 7,
                },
            )
        )
        asyncio.run(
            server.mcp.call_tool(
                "create_external_issue",
                {
                    "team-id": "ENG",
                    "title": "External ticket",
                },
            )
        )
        assert fake_router.official_calls == [
            (
                "create_issue",
                {
                    "title": "Ship it",
                    "description": None,
                    "unexpected-extra": 7,
                },
            ),
            (
                "create_external_issue",
                {
                    "team-id": "ENG",
                    "title": "External ticket",
                },
            ),
        ]
    finally:
        local_list_issues_tool.description = original_list_issues_description
        if "create_issue" in server.mcp._tool_manager._tools:
            server.mcp.remove_tool("create_issue")
        if "create_external_issue" in server.mcp._tool_manager._tools:
            server.mcp.remove_tool("create_external_issue")


def test_compose_official_tool_surface_returns_empty_on_list_error(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    fake_router = _FakeRouter()
    monkeypatch.setattr(server, "_router", fake_router)
    before = _tool_names()
    local_list_issues_tool = server.mcp._tool_manager._tools["list_issues"]
    original_list_issues_description = _tool_description("list_issues")

    class _BrokenOfficial:
        def list_tools(self):
            raise RuntimeError("official down")

    try:
        local_list_issues_tool.description = "stale synced description"

        assert server._compose_official_tool_surface(_BrokenOfficial()) == []
        after = _tool_names()
        assert after == before
        assert "list_issues" in after
        assert "call_linear_tool_raw" in after
        assert "official_call_tool" not in after
        assert "create_issue" not in after
        assert _tool_description("list_issues") == original_list_issues_description
    finally:
        local_list_issues_tool.description = original_list_issues_description


def test_compose_official_tool_surface_keeps_shared_local_description_when_official_is_blank(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    fake_router = _FakeRouter()
    monkeypatch.setattr(server, "_router", fake_router)
    original_list_issues_description = _tool_description("list_issues")

    class _FakeOfficial:
        def list_tools(self):
            return [
                {
                    "name": "create_issue",
                    "description": None,
                    "inputSchema": {"type": "object"},
                },
                {
                    "name": "list_issues",
                    "description": "   ",
                    "inputSchema": {"type": "object"},
                },
            ]

    if "create_issue" in server.mcp._tool_manager._tools:
        server.mcp.remove_tool("create_issue")

    try:
        added = server._compose_official_tool_surface(_FakeOfficial())

        assert added == ["create_issue"]
        assert _tool_description("list_issues") == original_list_issues_description
        assert server.mcp._tool_manager._tools["create_issue"].description == ""
    finally:
        server.mcp._tool_manager._tools["list_issues"].description = original_list_issues_description
        if "create_issue" in server.mcp._tool_manager._tools:
            server.mcp.remove_tool("create_issue")


def test_unregister_dynamic_official_tools_removes_registered_names(monkeypatch: pytest.MonkeyPatch) -> None:
    fake_router = _FakeRouter()
    monkeypatch.setattr(server, "_router", fake_router)

    if "create_issue" in server.mcp._tool_manager._tools:
        server.mcp.remove_tool("create_issue")

    server.mcp.add_tool(lambda title: {"title": title}, name="create_issue")
    monkeypatch.setattr(server, "_dynamic_official_tool_names", {"create_issue"})

    assert "create_issue" in server.mcp._tool_manager._tools
    server._unregister_dynamic_official_tools()
    assert "create_issue" not in server.mcp._tool_manager._tools
    assert server._dynamic_official_tool_names == set()
