from __future__ import annotations

import asyncio
import hashlib
from pathlib import Path
from unittest.mock import patch

import pytest

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


@pytest.fixture(autouse=True)
def _clear_official_env(monkeypatch: pytest.MonkeyPatch):
    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",
    ]
    for key in keys:
        monkeypatch.delenv(key, raising=False)


def test_default_transport_uses_stdio(monkeypatch: pytest.MonkeyPatch):
    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):
    monkeypatch.setenv(
        "LINEAR_OFFICIAL_MCP_ARGS",
        '["-y", "mcp-remote", "https://example.com/mcp", "--foo", "bar"]',
    )
    manager = OfficialMcpSessionManager()

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


def test_stdio_args_support_shell_style_string(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setenv(
        "LINEAR_OFFICIAL_MCP_ARGS",
        "-y mcp-remote https://example.com/mcp --name 'My Client'",
    )
    manager = OfficialMcpSessionManager()

    health = manager.get_health()
    assert health["args"] == [
        "-y",
        "mcp-remote",
        "https://example.com/mcp",
        "--name",
        "My Client",
    ]


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

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


def test_http_transport_keeps_legacy_header_mode(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setenv("LINEAR_OFFICIAL_MCP_TRANSPORT", "http")
    monkeypatch.setenv("LINEAR_OFFICIAL_MCP_HEADERS", '{"Authorization": "Bearer X"}')

    manager = OfficialMcpSessionManager()
    health = manager.get_health()

    assert health["transport"] == "http"
    assert health["url"] == DEFAULT_OFFICIAL_MCP_URL
    assert health["hasHeaders"] is True


def test_invalid_transport_raises(monkeypatch: pytest.MonkeyPatch):
    monkeypatch.setenv("LINEAR_OFFICIAL_MCP_TRANSPORT", "invalid")
    with pytest.raises(ValueError):
        OfficialMcpSessionManager()


def test_module_alias_runs_main():
    from oh_my_linearmcp import main as alias_main
    from oh_my_linear import main as base_main

    assert alias_main is base_main


class _FakeText:
    type = "text"

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


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


class _FakeTool:
    def __init__(self, name: str):
        self.name = name


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


def test_call_tool_preserves_official_tool_error(monkeypatch: pytest.MonkeyPatch):
    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, "_ensure_connected", lambda: None)
    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_list_tools_returns_empty_when_not_connected(monkeypatch: pytest.MonkeyPatch):
    manager = OfficialMcpSessionManager()

    # No session connected
    assert manager._session is None
    tools = manager.list_tools()
    assert tools == []


def test_list_tools_returns_names_when_connected(monkeypatch: pytest.MonkeyPatch):
    manager = OfficialMcpSessionManager()

    class _FakeSession:
        def list_tools(self):
            return _FakeResult(tools=[_FakeTool("create_issue"), _FakeTool("list_issues")])

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

    tools = manager.list_tools()
    assert tools == ["create_issue", "list_issues"]


def test_ensure_connected_uses_auth_timeout(monkeypatch: pytest.MonkeyPatch):
    manager = OfficialMcpSessionManager(auth_timeout_seconds=120.0)
    submitted_timeouts = []

    def fake_submit(coro, timeout=None):
        submitted_timeouts.append(timeout)
        if asyncio.iscoroutine(coro):
            coro.close()
        return None

    monkeypatch.setattr(manager, "_ensure_loop", lambda: None)
    monkeypatch.setattr(manager, "_submit", fake_submit)

    manager._ensure_connected()

    assert len(submitted_timeouts) == 1
    assert submitted_timeouts[0] == 130.0  # auth_timeout + 10


URL = "https://mcp.linear.app/mcp"
URL_HASH = hashlib.md5(URL.encode()).hexdigest()  # noqa: S324


class TestClearTokenCache:
    def test_deletes_matching_token_file(self, tmp_path: Path) -> None:
        cache_dir = tmp_path / ".mcp-auth" / "mcp-remote-0.1.37"
        cache_dir.mkdir(parents=True)
        (cache_dir / f"{URL_HASH}_tokens.json").write_text("test")
        (cache_dir / f"{URL_HASH}_client_info.json").write_text("test")
        (cache_dir / f"{URL_HASH}_code_verifier.txt").write_text("test")
        (cache_dir / "other_hash_tokens.json").write_text("keep")

        mgr = OfficialMcpSessionManager(url=URL, transport="stdio")
        with patch.object(Path, "home", return_value=tmp_path):
            deleted = mgr._clear_token_cache()

        assert deleted == 3
        assert not (cache_dir / f"{URL_HASH}_tokens.json").exists()
        assert not (cache_dir / f"{URL_HASH}_client_info.json").exists()
        assert not (cache_dir / f"{URL_HASH}_code_verifier.txt").exists()
        assert (cache_dir / "other_hash_tokens.json").exists()

    def test_no_error_when_no_files(self, tmp_path: Path) -> None:
        cache_dir = tmp_path / ".mcp-auth" / "mcp-remote-0.1.37"
        cache_dir.mkdir(parents=True)
        mgr = OfficialMcpSessionManager(url=URL, transport="stdio")
        with patch.object(Path, "home", return_value=tmp_path):
            deleted = mgr._clear_token_cache()
        assert deleted == 0

    def test_static_clear_for_url_works_without_instance(self, tmp_path: Path) -> None:
        cache_dir = tmp_path / ".mcp-auth" / "mcp-remote-0.1.37"
        cache_dir.mkdir(parents=True)
        (cache_dir / f"{URL_HASH}_tokens.json").write_text("test")
        (cache_dir / f"{URL_HASH}_client_info.json").write_text("test")
        (cache_dir / f"{URL_HASH}_code_verifier.txt").write_text("test")

        with patch.object(Path, "home", return_value=tmp_path):
            deleted = OfficialMcpSessionManager.clear_token_cache_for_url(URL)

        assert deleted == 3
        assert not (cache_dir / f"{URL_HASH}_tokens.json").exists()
        assert not (cache_dir / f"{URL_HASH}_client_info.json").exists()
        assert not (cache_dir / f"{URL_HASH}_code_verifier.txt").exists()

    def test_static_clear_defaults_to_official_url(self, tmp_path: Path) -> None:
        default_hash = hashlib.md5(DEFAULT_OFFICIAL_MCP_URL.encode()).hexdigest()  # noqa: S324
        cache_dir = tmp_path / ".mcp-auth" / "mcp-remote-0.1.37"
        cache_dir.mkdir(parents=True)
        (cache_dir / f"{default_hash}_tokens.json").write_text("test")

        with patch.object(Path, "home", return_value=tmp_path):
            deleted = OfficialMcpSessionManager.clear_token_cache_for_url()

        assert deleted == 1
        assert not (cache_dir / f"{default_hash}_tokens.json").exists()


class TestReauth:
    def test_reauth_clears_tokens_and_reconnects(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
        cache_dir = tmp_path / ".mcp-auth" / "mcp-remote-0.1.37"
        cache_dir.mkdir(parents=True)
        (cache_dir / f"{URL_HASH}_tokens.json").write_text("old-token")
        (cache_dir / f"{URL_HASH}_client_info.json").write_text("old-client-info")
        (cache_dir / f"{URL_HASH}_code_verifier.txt").write_text("old-verifier")

        mgr = OfficialMcpSessionManager(url=URL, transport="stdio")
        mgr._session = object()

        connect_called = {"count": 0}

        def fake_ensure_connected():
            connect_called["count"] += 1
            mgr._session = object()

        def fake_submit(coro, timeout=None):
            if asyncio.iscoroutine(coro):
                coro.close()
            mgr._session = None
            return None

        monkeypatch.setattr(mgr, "_ensure_connected", fake_ensure_connected)
        monkeypatch.setattr(mgr, "_submit", fake_submit)

        with patch.object(Path, "home", return_value=tmp_path):
            result = mgr.reauth()

        assert result["deletedTokenFiles"] == 3
        assert result["connected"] is True
        assert connect_called["count"] == 1
        assert not (cache_dir / f"{URL_HASH}_tokens.json").exists()
        assert not (cache_dir / f"{URL_HASH}_client_info.json").exists()
        assert not (cache_dir / f"{URL_HASH}_code_verifier.txt").exists()
