from __future__ import annotations

from typing import Any

import pytest

from oh_my_linear import local_handlers
from oh_my_linear.router import ToolRouter


class FakeReader:
    def __init__(self):
        self.refresh_count = 0

    def refresh_cache(self) -> None:
        self.refresh_count += 1

    def get_health(self) -> dict[str, Any]:
        return {"refreshCount": self.refresh_count}


class FakeOfficial:
    def __init__(self, connected: bool = True):
        self.calls: list[tuple[str, dict[str, Any]]] = []
        self.responses: dict[str, Any] = {}
        self.exceptions: dict[str, Exception] = {}
        self._connected = connected

    def is_connected(self) -> bool:
        return self._connected

    def call_tool(self, name: str, arguments: dict[str, Any] | None = None) -> Any:
        args = arguments or {}
        self.calls.append((name, args))
        if name in self.exceptions:
            raise self.exceptions[name]
        return self.responses.get(name, {"ok": True, "tool": name, "args": args})

    def list_tools(self) -> list[str]:
        return ["create_issue", "list_issues"]

    def get_health(self) -> dict[str, Any]:
        return {"connected": self._connected}


def _install_local_handler(monkeypatch: pytest.MonkeyPatch, fn):
    monkeypatch.setitem(local_handlers.LOCAL_READ_HANDLERS, "list_issues", fn)


# --- Local success: no official call ---


def test_read_local_success_without_official_call(monkeypatch: pytest.MonkeyPatch):
    reader = FakeReader()
    official = FakeOfficial()

    def handler(_reader, **_kwargs):
        return {"source": "local"}

    _install_local_handler(monkeypatch, handler)

    router = ToolRouter(reader, official)
    result = router.call_read("list_issues", {})

    assert result == {"source": "local"}
    assert official.calls == []


# --- Local fallback: unsupported tool falls back to official ---


def test_read_local_unsupported_falls_back_to_official(monkeypatch: pytest.MonkeyPatch):
    reader = FakeReader()
    official = FakeOfficial()

    def handler(_reader, **_kwargs):
        raise local_handlers.LocalFallbackRequested("unsupported_filter", "unsupported")

    _install_local_handler(monkeypatch, handler)

    router = ToolRouter(reader, official)
    result = router.call_read("list_issues", {"query": "hello"})

    assert result["tool"] == "list_issues"
    assert official.calls == [("list_issues", {"query": "hello"})]


# --- Unknown tool falls back to official ---


def test_unknown_tool_falls_back_to_official():
    reader = FakeReader()
    official = FakeOfficial()

    router = ToolRouter(reader, official)
    result = router.call_read("unknown_tool", {"foo": "bar"})

    assert result["tool"] == "unknown_tool"
    assert official.calls == [("unknown_tool", {"foo": "bar"})]


# --- refresh_cache called once per call_read ---


def test_call_read_calls_refresh_cache_once(monkeypatch: pytest.MonkeyPatch):
    reader = FakeReader()
    official = FakeOfficial()

    def handler(_reader, **_kwargs):
        return {"source": "local"}

    _install_local_handler(monkeypatch, handler)

    router = ToolRouter(reader, official)
    router.call_read("list_issues", {})
    router.call_read("list_issues", {})

    assert reader.refresh_count == 2


# --- call_official does not call refresh_cache ---


def test_call_official_does_not_refresh_cache():
    reader = FakeReader()
    official = FakeOfficial()

    router = ToolRouter(reader, official)
    router.call_official("create_issue", {"title": "T"})

    assert reader.refresh_count == 0
    assert official.calls == [("create_issue", {"title": "T"})]


# --- Unexpected local refresh failure falls back to official ---


def test_refresh_failure_falls_back_to_official(monkeypatch: pytest.MonkeyPatch):
    reader = FakeReader()
    official = FakeOfficial()

    def broken_refresh() -> None:
        raise RuntimeError("cache refresh crashed")

    monkeypatch.setattr(reader, "refresh_cache", broken_refresh)
    router = ToolRouter(reader, official)
    result = router.call_read("list_issues", {"team": "ENG"})

    assert result["tool"] == "list_issues"
    assert official.calls == [("list_issues", {"team": "ENG"})]


# --- Arguments are passed through ---


def test_arguments_passed_through_to_local_handler(monkeypatch: pytest.MonkeyPatch):
    reader = FakeReader()
    official = FakeOfficial()
    captured_kwargs = {}

    def handler(_reader, **kwargs):
        captured_kwargs.update(kwargs)
        return {"source": "local"}

    _install_local_handler(monkeypatch, handler)

    router = ToolRouter(reader, official)
    router.call_read("list_issues", {"team": "ENG", "limit": 10})

    assert captured_kwargs == {"team": "ENG", "limit": 10}


# --- None arguments default to empty dict ---


def test_call_read_with_none_arguments(monkeypatch: pytest.MonkeyPatch):
    reader = FakeReader()
    official = FakeOfficial()

    def handler(_reader, **_kwargs):
        return {"source": "local"}

    _install_local_handler(monkeypatch, handler)

    router = ToolRouter(reader, official)
    result = router.call_read("list_issues")

    assert result == {"source": "local"}


def test_call_official_with_none_arguments():
    reader = FakeReader()
    official = FakeOfficial()

    router = ToolRouter(reader, official)
    router.call_official("create_issue")

    assert official.calls == [("create_issue", {})]


def test_unexpected_local_handler_error_is_propagated(monkeypatch: pytest.MonkeyPatch):
    reader = FakeReader()
    official = FakeOfficial()

    def handler(_reader, **_kwargs):
        raise ValueError("bad local state")

    _install_local_handler(monkeypatch, handler)
    router = ToolRouter(reader, official)

    with pytest.raises(ValueError):
        router.call_read("list_issues", {"query": "x"})
    assert official.calls == []


# --- Local fallback attempts official regardless of preexisting connection state ---


def test_local_fallback_attempts_official_when_not_connected(monkeypatch: pytest.MonkeyPatch):
    reader = FakeReader()
    official = FakeOfficial(connected=False)

    def handler(_reader, **_kwargs):
        raise local_handlers.LocalFallbackRequested("unsupported", "nope")

    _install_local_handler(monkeypatch, handler)

    router = ToolRouter(reader, official)
    result = router.call_read("list_issues", {})

    assert result["tool"] == "list_issues"
    assert official.calls == [("list_issues", {})]


def test_local_fallback_works_when_official_connected(monkeypatch: pytest.MonkeyPatch):
    reader = FakeReader()
    official = FakeOfficial(connected=True)

    def handler(_reader, **_kwargs):
        raise local_handlers.LocalFallbackRequested("unsupported", "nope")

    _install_local_handler(monkeypatch, handler)

    router = ToolRouter(reader, official)
    result = router.call_read("list_issues", {})

    assert result["tool"] == "list_issues"
    assert len(official.calls) == 1
