from __future__ import annotations

import asyncio
from pathlib import Path

import pytest

from oh_my_linear.official_session import (
    DEFAULT_OFFICIAL_MCP_URL,
    OfficialMcpSessionManager,
    OfficialToolError,
)


@pytest.fixture(autouse=True)
def _clear_official_env(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
    keys = [
        "LINEAR_OFFICIAL_MCP_TRANSPORT",
        "LINEAR_OFFICIAL_MCP_COMMAND",
        "LINEAR_OFFICIAL_MCP_ARGS",
        "LINEAR_OFFICIAL_MCP_ENV",
        "LINEAR_OFFICIAL_MCP_CWD",
        "LINEAR_OFFICIAL_MCP_URL",
        "LINEAR_OFFICIAL_MCP_HEADERS",
        "LINEAR_OFFICIAL_AUTH_RETRY_COOLDOWN_SECONDS",
    ]
    for key in keys:
        monkeypatch.delenv(key, raising=False)


def test_default_transport_uses_stdio() -> None:
    manager = OfficialMcpSessionManager()
    health = manager.get_health()

    assert health["transport"] == "stdio"
    assert health["command"] == "npx"
    assert health["args"] == ["-y", "mcp-remote", DEFAULT_OFFICIAL_MCP_URL]


def test_stdio_args_support_json_array(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv(
        "LINEAR_OFFICIAL_MCP_ARGS",
        '["-y", "mcp-remote", "https://example.com/mcp", "--foo", "bar"]',
    )
    manager = OfficialMcpSessionManager()

    assert manager.get_health()["args"] == [
        "-y",
        "mcp-remote",
        "https://example.com/mcp",
        "--foo",
        "bar",
    ]


def test_stdio_args_invalid_shell_string_falls_back_to_default(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LINEAR_OFFICIAL_MCP_ARGS", "-y mcp-remote 'unterminated")
    manager = OfficialMcpSessionManager()

    assert manager.get_health()["args"] == ["-y", "mcp-remote", DEFAULT_OFFICIAL_MCP_URL]


def test_invalid_transport_raises(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv("LINEAR_OFFICIAL_MCP_TRANSPORT", "invalid")

    with pytest.raises(ValueError):
        OfficialMcpSessionManager()


class _FakeText:
    type = "text"

    def __init__(self, text: str):
        self.text = text


class _FakeResult:
    def __init__(self, *, is_error: bool = False, text: str = ""):
        self.isError = is_error
        self.content = [_FakeText(text)] if text else []


class _FakeToolSpec:
    def __init__(self, payload: dict[str, object]):
        self._payload = payload

    def model_dump(self):
        return self._payload


class _FakeListToolsResult:
    def __init__(self, tools, next_cursor: str | None = None):
        self.tools = tools
        self.nextCursor = next_cursor


def _sync_submit(value):
    if asyncio.iscoroutine(value):
        return asyncio.run(value)
    return value


def test_call_tool_success_normalizes_json_text(monkeypatch: pytest.MonkeyPatch) -> None:
    manager = OfficialMcpSessionManager()

    class _FakeSession:
        def call_tool(self, name: str, arguments=None):
            return _FakeResult(text='{"ok": true}')

    manager._session = _FakeSession()
    monkeypatch.setattr(manager, "_submit", _sync_submit)

    assert manager.call_tool("list_issues", {}) == {"ok": True}


def test_call_tool_preserves_official_tool_error(monkeypatch: pytest.MonkeyPatch) -> None:
    manager = OfficialMcpSessionManager()

    class _FakeSession:
        def call_tool(self, name: str, arguments=None):
            return _FakeResult(is_error=True, text="official said no")

    manager._session = _FakeSession()
    monkeypatch.setattr(manager, "_submit", _sync_submit)

    with pytest.raises(OfficialToolError) as exc_info:
        manager.call_tool("list_issues", {})

    assert exc_info.value.code == "official_tool_error"
    assert "official said no" in exc_info.value.message


def test_call_tool_retries_once_after_connect_failure(monkeypatch: pytest.MonkeyPatch) -> None:
    manager = OfficialMcpSessionManager()
    connect_calls = {"count": 0}

    class _FakeSession:
        def call_tool(self, name: str, arguments=None):
            return _FakeResult(text='{"ok": true}')

    def fake_connect_locked():
        connect_calls["count"] += 1
        if connect_calls["count"] == 1:
            raise RuntimeError("connect failed once")
        manager._session = _FakeSession()

    monkeypatch.setattr(manager, "_connect_locked", fake_connect_locked)
    monkeypatch.setattr(manager, "_safe_disconnect", lambda message: None)
    monkeypatch.setattr(manager, "_submit", _sync_submit)

    assert manager.call_tool("list_issues", {}) == {"ok": True}
    assert connect_calls["count"] == 2


def test_call_tool_raises_unavailable_after_second_failure(monkeypatch: pytest.MonkeyPatch) -> None:
    manager = OfficialMcpSessionManager()

    def fake_connect_locked():
        raise RuntimeError("still failing")

    monkeypatch.setattr(manager, "_connect_locked", fake_connect_locked)
    monkeypatch.setattr(manager, "_safe_disconnect", lambda message: None)

    with pytest.raises(OfficialToolError) as exc_info:
        manager.call_tool("list_issues", {})

    assert exc_info.value.code == "official_unavailable"
    assert "still failing" in exc_info.value.message


def test_call_tool_sets_auth_cooldown_after_auth_failure(monkeypatch: pytest.MonkeyPatch) -> None:
    manager = OfficialMcpSessionManager(auth_retry_cooldown_seconds=60)
    connect_calls = {"count": 0}

    def fake_connect_locked():
        connect_calls["count"] += 1
        raise TimeoutError("oauth timed out")

    monkeypatch.setattr(manager, "_connect_locked", fake_connect_locked)
    monkeypatch.setattr(manager, "_safe_disconnect", lambda message: None)

    with pytest.raises(OfficialToolError) as first_exc:
        manager.call_tool("list_issues", {})
    assert first_exc.value.code == "official_auth_required"
    assert "/mcp" in first_exc.value.message
    assert "oh-my-linear" in first_exc.value.message

    with pytest.raises(OfficialToolError) as second_exc:
        manager.call_tool("list_issues", {})
    assert second_exc.value.code == "official_auth_cooldown"
    assert "/mcp" in second_exc.value.message
    assert connect_calls["count"] == 1
    assert manager.get_health()["authRetryRemainingSeconds"] > 0


def test_connect_auth_error_includes_reauth_instructions(monkeypatch: pytest.MonkeyPatch) -> None:
    manager = OfficialMcpSessionManager()

    monkeypatch.setattr(manager, "_connect_locked", lambda: (_ for _ in ()).throw(TimeoutError("oauth timed out")))
    monkeypatch.setattr(manager, "_safe_disconnect", lambda message: None)

    with pytest.raises(OfficialToolError) as exc_info:
        manager.connect()

    assert exc_info.value.code == "official_auth_required"
    assert "/mcp" in exc_info.value.message
    assert "oh-my-linear" in exc_info.value.message


def test_connect_wraps_errors_as_official_unavailable(monkeypatch: pytest.MonkeyPatch) -> None:
    manager = OfficialMcpSessionManager()

    monkeypatch.setattr(manager, "_connect_locked", lambda: (_ for _ in ()).throw(RuntimeError("boom")))
    monkeypatch.setattr(manager, "_safe_disconnect", lambda message: None)

    with pytest.raises(OfficialToolError) as exc_info:
        manager.connect()

    assert exc_info.value.code == "official_unavailable"
    assert "connect failed" in exc_info.value.message


def test_get_health_redacts_sensitive_values(monkeypatch: pytest.MonkeyPatch) -> None:
    monkeypatch.setenv(
        "LINEAR_OFFICIAL_MCP_ARGS",
        "-y mcp-remote https://mcp.linear.app/mcp --header Authorization:Bearer super-secret-token",
    )
    manager = OfficialMcpSessionManager()
    manager._last_error = "RuntimeError: token=super-secret-token"
    manager._last_auth_failure_reason = "TimeoutError: oauth token=super-secret-token"
    manager._auth_retry_not_before = 9999999999.0

    health = manager.get_health()
    args_text = " ".join(health["args"])

    assert "super-secret-token" not in args_text
    assert "super-secret-token" not in (health["lastError"] or "")
    assert "super-secret-token" not in (health["lastAuthFailureReason"] or "")
    assert health["authRetryRemainingSeconds"] > 0


def test_list_tools_normalizes_specs(monkeypatch: pytest.MonkeyPatch) -> None:
    manager = OfficialMcpSessionManager()

    class _FakeSession:
        def list_tools(self):
            return _FakeListToolsResult(
                [
                    _FakeToolSpec(
                        {
                            "name": "create_issue",
                            "description": "Create issue",
                            "inputSchema": {
                                "type": "object",
                                "properties": {"title": {"type": "string"}},
                                "required": ["title"],
                            },
                        }
                    ),
                    {
                        "name": "update_issue",
                        "description": "",
                        "inputSchema": {"type": "object"},
                    },
                ]
            )

    manager._session = _FakeSession()
    monkeypatch.setattr(manager, "_submit", _sync_submit)

    tools = manager.list_tools()

    assert len(tools) == 2
    assert tools[0]["name"] == "create_issue"
    assert tools[0]["description"] == "Create issue"
    assert tools[0]["inputSchema"]["required"] == ["title"]
    assert tools[1]["name"] == "update_issue"
    assert tools[1]["inputSchema"]["type"] == "object"


def test_list_tools_raises_unavailable_after_second_failure(monkeypatch: pytest.MonkeyPatch) -> None:
    manager = OfficialMcpSessionManager()

    def fake_connect_locked():
        raise RuntimeError("still failing")

    monkeypatch.setattr(manager, "_connect_locked", fake_connect_locked)
    monkeypatch.setattr(manager, "_safe_disconnect", lambda message: None)

    with pytest.raises(OfficialToolError) as exc_info:
        manager.list_tools()

    assert exc_info.value.code == "official_unavailable"
    assert "list_tools failed" in exc_info.value.message


def test_list_tools_supports_cursor_pagination(monkeypatch: pytest.MonkeyPatch) -> None:
    manager = OfficialMcpSessionManager()

    class _FakeSession:
        def list_tools(self, cursor: str | None = None):
            if cursor is None:
                return _FakeListToolsResult(
                    [
                        {
                            "name": "create_issue",
                            "description": "Create issue",
                            "inputSchema": {"type": "object"},
                        }
                    ],
                    next_cursor="page-2",
                )
            if cursor == "page-2":
                return _FakeListToolsResult(
                    [
                        {
                            "name": "update_issue",
                            "description": "Update issue",
                            "inputSchema": {"type": "object"},
                        }
                    ]
                )
            return _FakeListToolsResult([])

    manager._session = _FakeSession()
    monkeypatch.setattr(manager, "_submit", _sync_submit)

    tools = manager.list_tools()
    names = [tool["name"] for tool in tools]
    assert names == ["create_issue", "update_issue"]
