"""Tests for skills v2.1: SkillKey, SkillRef, and Session.invoke_skill()."""

import pytest

from meerkat import MeerkatClient, SkillKey
from meerkat.session import DeferredSession, Session, _normalize_skill_ref
from meerkat.types import RunResult, SessionHistory, SessionMessage, Usage


# ---------------------------------------------------------------------------
# SkillKey and normalization
# ---------------------------------------------------------------------------

def test_skill_key_is_frozen():
    key = SkillKey(source_uuid="abc", skill_name="email-extractor")
    assert key.source_uuid == "abc"
    assert key.skill_name == "email-extractor"
    with pytest.raises(AttributeError):
        key.source_uuid = "xyz"  # type: ignore[misc]


def test_normalize_skill_ref_passes_skill_key_through():
    key = SkillKey(source_uuid="abc", skill_name="tool")
    result = _normalize_skill_ref(key)
    assert result is key


def test_normalize_skill_ref_rejects_legacy_string():
    with pytest.raises(TypeError, match="Skill references must be SkillKey objects"):
        _normalize_skill_ref("dc256086-0d2f-4f61-a307-320d4148107f/email-extractor")  # type: ignore[arg-type]


# ---------------------------------------------------------------------------
# Session.invoke_skill() integration (mock)
# ---------------------------------------------------------------------------

class _MockClient:
    """Minimal mock of MeerkatClient for testing Session."""

    def __init__(self):
        self._calls: list[dict] = []
        self._send_calls: list[dict] = []
        self._peers_calls: list[str] = []
        self._history_calls: list[dict] = []

    def has_capability(self, cap: str) -> bool:
        return cap == "skills"

    def require_capability(self, cap: str) -> None:
        if not self.has_capability(cap):
            raise RuntimeError(f"Missing capability: {cap}")

    async def _start_turn(
        self,
        session_id,
        prompt,
        *,
        skill_refs=None,
        flow_tool_overlay=None,
        additional_instructions=None,
        keep_alive=None,
        model=None,
        provider=None,
        max_tokens=None,
        system_prompt=None,
        output_schema=None,
        structured_output_retries=None,
        provider_params=None,
    ):
        self._calls.append({
            "session_id": session_id,
            "prompt": prompt,
            "skill_refs": skill_refs,
            "flow_tool_overlay": flow_tool_overlay,
            "additional_instructions": additional_instructions,
            "keep_alive": keep_alive,
            "model": model,
            "provider": provider,
            "max_tokens": max_tokens,
            "system_prompt": system_prompt,
            "output_schema": output_schema,
            "structured_output_retries": structured_output_retries,
            "provider_params": provider_params,
        })
        return RunResult(
            session_id=session_id,
            text="ok",
            usage=Usage(input_tokens=10, output_tokens=5),
        )

    async def _send(self, session_id, **kwargs):
        self._send_calls.append({"session_id": session_id, "kwargs": kwargs})
        return {"queued": True, "echo": kwargs}

    async def _peers(self, session_id):
        self._peers_calls.append(session_id)
        return {
            "peers": [
                {"id": "peer-a", "name": "alpha"},
                {"id": "peer-b", "name": "beta"},
            ]
        }

    async def read_session_history(self, session_id, *, offset=0, limit=None):
        self._history_calls.append({
            "session_id": session_id,
            "offset": offset,
            "limit": limit,
        })
        return SessionHistory(
            session_id=session_id,
            session_ref="ref-1",
            message_count=2,
            offset=offset,
            limit=limit,
            has_more=False,
            messages=[
                SessionMessage(role="user", content="hello"),
                SessionMessage(role="assistant", content="ok", stop_reason="end_turn"),
            ],
        )

def _make_session() -> tuple[Session, _MockClient]:
    client = _MockClient()
    initial = RunResult(session_id="s-1", text="init", usage=Usage())
    return Session(client, initial), client


@pytest.mark.asyncio
async def test_invoke_skill_with_skill_key():
    session, client = _make_session()
    key = SkillKey(source_uuid="abc-123", skill_name="email-extractor")
    result = await session.invoke_skill(key, "run the extractor")

    assert result.text == "ok"
    assert len(client._calls) == 1
    call = client._calls[0]
    assert call["prompt"] == "run the extractor"
    assert call["skill_refs"] == [key]


@pytest.mark.asyncio
async def test_invoke_skill_rejects_legacy_string():
    session, client = _make_session()
    with pytest.raises(TypeError, match="Skill references must be SkillKey objects"):
        await session.invoke_skill(
            "dc256086-0d2f-4f61-a307-320d4148107f/email-extractor",
            "run",
        )  # type: ignore[arg-type]

    assert client._calls == []


@pytest.mark.asyncio
async def test_session_send_routes_to_client_send():
    session, client = _make_session()
    result = await session.send(kind="peer_message", to="agent-b", body="hello")

    assert result["queued"] is True
    assert len(client._send_calls) == 1
    assert client._send_calls[0]["session_id"] == "s-1"
    assert client._send_calls[0]["kwargs"] == {
        "kind": "peer_message",
        "to": "agent-b",
        "body": "hello",
    }


@pytest.mark.asyncio
async def test_session_send_routes_peer_lifecycle_command():
    session, client = _make_session()
    result = await session.send(
        kind="peer_lifecycle",
        to="agent-b",
        lifecycle_kind="mob.peer_retired",
        params={"reason": "done"},
    )

    assert result["queued"] is True
    assert len(client._send_calls) == 1
    assert client._send_calls[0]["session_id"] == "s-1"
    assert client._send_calls[0]["kwargs"] == {
        "kind": "peer_lifecycle",
        "to": "agent-b",
        "lifecycle_kind": "mob.peer_retired",
        "params": {"reason": "done"},
    }


@pytest.mark.asyncio
async def test_session_peers_returns_peer_list():
    session, client = _make_session()
    peers = await session.peers()

    assert len(client._peers_calls) == 1
    assert client._peers_calls[0] == "s-1"
    assert peers == [
        {"id": "peer-a", "name": "alpha"},
        {"id": "peer-b", "name": "beta"},
    ]


@pytest.mark.asyncio
async def test_session_history_routes_to_client_history_reader():
    session, client = _make_session()
    history = await session.history(offset=1, limit=5)

    assert history.session_id == "s-1"
    assert history.messages[1].role == "assistant"
    assert client._history_calls == [
        {"session_id": "s-1", "offset": 1, "limit": 5},
    ]


@pytest.mark.asyncio
async def test_deferred_session_history_routes_to_client_history_reader():
    client = _MockClient()
    session = DeferredSession(client, "s-2", "ref-2")

    history = await session.history(offset=2, limit=3)

    assert history.session_id == "s-2"
    assert history.session_ref == "ref-1"
    assert client._history_calls == [
        {"session_id": "s-2", "offset": 2, "limit": 3},
    ]


@pytest.mark.asyncio
async def test_client_create_mob_uses_typed_route():
    client = object.__new__(MeerkatClient)
    calls: list[tuple[str, dict]] = []

    async def fake_request(method, params):
        calls.append((method, params))
        if method == "mob/create":
            return {"ok": True, "mob_id": "m1"}
        return {}

    client._request = fake_request  # type: ignore[attr-defined]
    client._methods = {"mob/create"}  # type: ignore[attr-defined]
    client._capabilities = []  # type: ignore[attr-defined]
    mob = await MeerkatClient.create_mob(
        client,
        definition={"id": "mob-1", "profiles": {"worker": {"model": "claude-sonnet-4-6"}}},
    )
    assert mob.id == "m1"
    assert calls == [
        ("mob/create", {"definition": {"id": "mob-1", "profiles": {"worker": {"model": "claude-sonnet-4-6"}}}}),
    ]


@pytest.mark.asyncio
async def test_set_config_returns_envelope():
    client = object.__new__(MeerkatClient)
    calls: list[tuple[str, dict]] = []

    async def fake_request(method, params):
        calls.append((method, params))
        return {"config": {"agent": {"model": "x"}}, "generation": 7}

    client._request = fake_request  # type: ignore[attr-defined]
    envelope = await MeerkatClient.set_config(
        client,
        {"agent": {"model": "x"}},
        expected_generation=6,
    )
    assert envelope["generation"] == 7
    assert calls == [
        ("config/set", {"config": {"agent": {"model": "x"}}, "expected_generation": 6})
    ]
