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 []


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"

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


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
