from __future__ import annotations

import json
import time
from pathlib import Path

import pytest
from fastmcp import Client

from claude_teams import messaging, tasks, teams
from claude_teams.models import TeammateMember
from claude_teams.server import (
    _build_check_teammate_description,
    _build_read_inbox_description,
    mcp,
)


def _make_teammate(
    name: str,
    team_name: str,
) -> TeammateMember:
    return TeammateMember(
        agent_id=f"{name}@{team_name}",
        name=name,
        agent_type="teammate",
        model="claude-sonnet-4-20250514",
        color="blue",
        plan_mode_required=False,
        joined_at=int(time.time() * 1000),
        acpx_session_name=f"{name}@{team_name}",
        cwd="/tmp",
    )


@pytest.fixture
async def client(tmp_path: Path, monkeypatch):
    monkeypatch.setattr(teams, "TEAMS_DIR", tmp_path / "teams")
    monkeypatch.setattr(teams, "TASKS_DIR", tmp_path / "tasks")
    monkeypatch.setattr(tasks, "TASKS_DIR", tmp_path / "tasks")
    monkeypatch.setattr(messaging, "TEAMS_DIR", tmp_path / "teams")
    monkeypatch.setattr(
        "claude_teams.server.discover_harness_binary",
        lambda name: "/usr/bin/echo" if name == "acpx" else None,
    )
    # Mock shutil.which so app_lifespan finds acpx even when not installed
    monkeypatch.setattr(
        "claude_teams.server.shutil.which",
        lambda name: "/usr/bin/echo" if name == "acpx" else None,
    )
    # Mock acpx_client calls to avoid real subprocess invocations
    from claude_teams import acpx_client as _acpx
    from claude_teams.acpx_client import AcpxError
    monkeypatch.setattr(_acpx, "get_session_status", lambda agent, session_name, cwd, acpx_binary="acpx", timeout=30: (_ for _ in ()).throw(AcpxError("session not found")))
    monkeypatch.setattr(_acpx, "close_session", lambda agent, session_name, cwd, acpx_binary="acpx", timeout=30: None)
    (tmp_path / "teams").mkdir()
    (tmp_path / "tasks").mkdir()
    async with Client(mcp) as c:
        yield c


def _data(result):
    """Extract raw Python data from a successful CallToolResult."""
    if result.content:
        return json.loads(result.content[0].text)
    return result.data


class TestErrorPropagation:
    async def test_should_reject_duplicate_team_name(self, client: Client):
        await client.call_tool("team_create", {"team_name": "alpha"})
        result = await client.call_tool(
            "team_create", {"team_name": "alpha"}, raise_on_error=False
        )
        assert result.is_error is True
        assert "alpha" in result.content[0].text

    async def test_should_reject_unknown_agent_in_force_kill(self, client: Client):
        await client.call_tool("team_create", {"team_name": "t1"})
        result = await client.call_tool(
            "force_kill_teammate",
            {"team_name": "t1", "agent_name": "ghost"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "ghost" in result.content[0].text

    async def test_should_reject_invalid_message_type(self, client: Client):
        await client.call_tool("team_create", {"team_name": "t_msg"})
        result = await client.call_tool(
            "send_message",
            {"team_name": "t_msg", "type": "bogus"},
            raise_on_error=False,
        )
        assert result.is_error is True


class TestDeletedTaskGuard:
    async def test_should_not_send_assignment_when_task_deleted(self, client: Client):
        await client.call_tool("team_create", {"team_name": "t2"})
        teams.add_member("t2", _make_teammate("worker", "t2"))
        created = _data(
            await client.call_tool(
                "task_create",
                {"team_name": "t2", "subject": "doomed", "description": "will delete"},
            )
        )
        await client.call_tool(
            "task_update",
            {
                "team_name": "t2",
                "task_id": created["id"],
                "status": "deleted",
                "owner": "worker",
            },
        )
        inbox = _data(
            await client.call_tool(
                "read_inbox", {"team_name": "t2", "agent_name": "worker"}
            )
        )
        assert inbox == []

    async def test_should_send_assignment_when_owner_set_on_live_task(
        self, client: Client
    ):
        await client.call_tool("team_create", {"team_name": "t2b"})
        teams.add_member("t2b", _make_teammate("worker", "t2b"))
        created = _data(
            await client.call_tool(
                "task_create",
                {"team_name": "t2b", "subject": "live", "description": "stays"},
            )
        )
        await client.call_tool(
            "task_update",
            {"team_name": "t2b", "task_id": created["id"], "owner": "worker"},
        )
        inbox = _data(
            await client.call_tool(
                "read_inbox", {"team_name": "t2b", "agent_name": "worker"}
            )
        )
        assert len(inbox) == 1
        payload = json.loads(inbox[0]["text"])
        assert payload["type"] == "task_assignment"
        assert payload["taskId"] == created["id"]


class TestShutdownResponseSender:
    async def test_should_populate_correct_from_and_acpx_session_name_on_approve(
        self, client: Client
    ):
        await client.call_tool("team_create", {"team_name": "t3"})
        teams.add_member("t3", _make_teammate("worker", "t3"))
        await client.call_tool(
            "send_message",
            {
                "team_name": "t3",
                "type": "shutdown_response",
                "sender": "worker",
                "request_id": "req-1",
                "approve": True,
            },
        )
        inbox = _data(
            await client.call_tool(
                "read_inbox", {"team_name": "t3", "agent_name": "team-lead"}
            )
        )
        assert len(inbox) == 1
        payload = json.loads(inbox[0]["text"])
        assert payload["type"] == "shutdown_approved"
        assert payload["from"] == "worker"
        assert payload["acpxSessionName"] == "worker@t3"
        assert payload["requestId"] == "req-1"

    async def test_should_attribute_rejection_to_sender(self, client: Client):
        await client.call_tool("team_create", {"team_name": "t3b"})
        teams.add_member("t3b", _make_teammate("rebel", "t3b"))
        await client.call_tool(
            "send_message",
            {
                "team_name": "t3b",
                "type": "shutdown_response",
                "sender": "rebel",
                "request_id": "req-2",
                "approve": False,
                "content": "still busy",
            },
        )
        inbox = _data(
            await client.call_tool(
                "read_inbox", {"team_name": "t3b", "agent_name": "team-lead"}
            )
        )
        assert len(inbox) == 1
        assert inbox[0]["from"] == "rebel"
        assert inbox[0]["text"] == "still busy"

    async def test_should_reject_shutdown_response_from_non_member(
        self, client: Client
    ):
        await client.call_tool("team_create", {"team_name": "t3c"})
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "t3c",
                "type": "shutdown_response",
                "sender": "ghost",
                "request_id": "req-ghost",
                "approve": True,
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "sender" in result.content[0].text.lower()
        assert "ghost" in result.content[0].text


class TestPlanApprovalSender:
    async def test_should_use_sender_as_from_on_approve(self, client: Client):
        await client.call_tool("team_create", {"team_name": "t_plan"})
        teams.add_member("t_plan", _make_teammate("dev", "t_plan"))
        await client.call_tool(
            "send_message",
            {
                "team_name": "t_plan",
                "type": "plan_approval_response",
                "sender": "team-lead",
                "recipient": "dev",
                "request_id": "plan-1",
                "approve": True,
            },
        )
        inbox = _data(
            await client.call_tool(
                "read_inbox", {"team_name": "t_plan", "agent_name": "dev"}
            )
        )
        assert len(inbox) == 1
        assert inbox[0]["from"] == "team-lead"
        payload = json.loads(inbox[0]["text"])
        assert payload["type"] == "plan_approval"
        assert payload["approved"] is True

    async def test_should_use_sender_as_from_on_reject(self, client: Client):
        await client.call_tool("team_create", {"team_name": "t_plan2"})
        teams.add_member("t_plan2", _make_teammate("dev2", "t_plan2"))
        await client.call_tool(
            "send_message",
            {
                "team_name": "t_plan2",
                "type": "plan_approval_response",
                "sender": "team-lead",
                "recipient": "dev2",
                "approve": False,
                "content": "needs error handling",
            },
        )
        inbox = _data(
            await client.call_tool(
                "read_inbox", {"team_name": "t_plan2", "agent_name": "dev2"}
            )
        )
        assert len(inbox) == 1
        assert inbox[0]["from"] == "team-lead"
        assert inbox[0]["text"] == "needs error handling"


class TestWiring:
    async def test_should_round_trip_task_create_and_list(self, client: Client):
        await client.call_tool("team_create", {"team_name": "t4"})
        await client.call_tool(
            "task_create",
            {"team_name": "t4", "subject": "first", "description": "d1"},
        )
        await client.call_tool(
            "task_create",
            {"team_name": "t4", "subject": "second", "description": "d2"},
        )
        result = _data(await client.call_tool("task_list", {"team_name": "t4"}))
        assert len(result) == 2
        assert result[0]["subject"] == "first"
        assert result[1]["subject"] == "second"

    async def test_should_round_trip_send_message_and_read_inbox(self, client: Client):
        await client.call_tool("team_create", {"team_name": "t5"})
        teams.add_member("t5", _make_teammate("bob", "t5"))
        await client.call_tool(
            "send_message",
            {
                "team_name": "t5",
                "type": "message",
                "recipient": "bob",
                "content": "hello bob",
                "summary": "greeting",
            },
        )
        inbox = _data(
            await client.call_tool(
                "read_inbox", {"team_name": "t5", "agent_name": "bob"}
            )
        )
        assert len(inbox) == 1
        assert inbox[0]["text"].startswith("hello bob")
        assert "sent from team-lead" in inbox[0]["text"]
        assert inbox[0]["from"] == "team-lead"

    async def test_should_round_trip_teammate_message_to_team_lead_with_sender(
        self, client: Client
    ):
        await client.call_tool("team_create", {"team_name": "t5b"})
        teams.add_member("t5b", _make_teammate("worker", "t5b"))
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "t5b",
                "type": "message",
                "sender": "worker",
                "recipient": "team-lead",
                "content": "done",
                "summary": "status",
            },
        )
        data = _data(result)
        assert data["routing"]["sender"] == "worker"
        inbox = _data(
            await client.call_tool(
                "read_inbox", {"team_name": "t5b", "agent_name": "team-lead"}
            )
        )
        assert len(inbox) == 1
        assert inbox[0]["from"] == "worker"
        assert inbox[0]["text"].startswith("done")
        assert "sent from worker" in inbox[0]["text"]


class TestTeamDeleteClearsSession:
    async def test_should_allow_new_team_after_delete(self, client: Client):
        await client.call_tool("team_create", {"team_name": "first"})
        await client.call_tool("team_delete", {"team_name": "first"})
        result = await client.call_tool("team_create", {"team_name": "second"})
        data = _data(result)
        assert data["team_name"] == "second"


class TestSendMessageValidation:
    async def test_should_allow_empty_content(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv1"})
        teams.add_member("tv1", _make_teammate("bob", "tv1"))
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tv1",
                "type": "message",
                "recipient": "bob",
                "content": "",
                "summary": "hi",
            },
            raise_on_error=False,
        )
        assert result.is_error is not True

    async def test_should_reject_empty_summary(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv2"})
        teams.add_member("tv2", _make_teammate("bob", "tv2"))
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tv2",
                "type": "message",
                "recipient": "bob",
                "content": "hi",
                "summary": "",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "summary" in result.content[0].text.lower()

    async def test_should_reject_empty_recipient(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv3"})
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tv3",
                "type": "message",
                "recipient": "",
                "content": "hi",
                "summary": "hi",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "recipient" in result.content[0].text.lower()

    async def test_should_reject_nonexistent_recipient(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv4"})
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tv4",
                "type": "message",
                "recipient": "ghost",
                "content": "hi",
                "summary": "hi",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "ghost" in result.content[0].text

    async def test_should_pass_target_color(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv5"})
        teams.add_member("tv5", _make_teammate("bob", "tv5"))
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tv5",
                "type": "message",
                "recipient": "bob",
                "content": "hey",
                "summary": "greet",
            },
        )
        data = _data(result)
        assert data["routing"]["targetColor"] == "blue"

    async def test_should_reject_broadcast_empty_summary(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv6"})
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tv6",
                "type": "broadcast",
                "content": "hello",
                "summary": "",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "summary" in result.content[0].text.lower()

    async def test_should_reject_shutdown_request_to_team_lead(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv7"})
        result = await client.call_tool(
            "send_message",
            {"team_name": "tv7", "type": "shutdown_request", "recipient": "team-lead"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "team-lead" in result.content[0].text

    async def test_should_reject_shutdown_request_to_nonexistent(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv8"})
        result = await client.call_tool(
            "send_message",
            {"team_name": "tv8", "type": "shutdown_request", "recipient": "ghost"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "ghost" in result.content[0].text

    async def test_should_reject_teammate_to_teammate_message(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv9"})
        teams.add_member("tv9", _make_teammate("alice", "tv9"))
        teams.add_member("tv9", _make_teammate("bob", "tv9"))
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tv9",
                "type": "message",
                "sender": "alice",
                "recipient": "bob",
                "content": "hi",
                "summary": "note",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "team-lead" in result.content[0].text

    async def test_should_reject_self_message(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv_self"})
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tv_self",
                "type": "message",
                "sender": "team-lead",
                "recipient": "team-lead",
                "content": "talking to myself",
                "summary": "self",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "yourself" in result.content[0].text.lower()

    async def test_should_reject_owner_not_in_team(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv_own"})
        created = _data(
            await client.call_tool(
                "task_create",
                {"team_name": "tv_own", "subject": "x", "description": "d"},
            )
        )
        result = await client.call_tool(
            "task_update",
            {"team_name": "tv_own", "task_id": created["id"], "owner": "ghost"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "ghost" in result.content[0].text

    async def test_should_reject_read_inbox_for_nonexistent_agent(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv_ri"})
        result = await client.call_tool(
            "read_inbox",
            {"team_name": "tv_ri", "agent_name": "ghost"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "ghost" in result.content[0].text

    async def test_should_reject_non_lead_broadcast(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tv10"})
        teams.add_member("tv10", _make_teammate("alice", "tv10"))
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tv10",
                "type": "broadcast",
                "sender": "alice",
                "content": "hello",
                "summary": "heads-up",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "team-lead" in result.content[0].text.lower()


class TestProcessShutdownGuard:
    async def test_should_reject_shutdown_of_team_lead(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tsg"})
        result = await client.call_tool(
            "process_shutdown_approved",
            {"team_name": "tsg", "agent_name": "team-lead"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "team-lead" in result.content[0].text

    async def test_should_reject_shutdown_of_nonexistent_agent(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tsg2"})
        result = await client.call_tool(
            "process_shutdown_approved",
            {"team_name": "tsg2", "agent_name": "ghost"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "ghost" in result.content[0].text

    async def test_should_remove_member_on_shutdown(self, client: Client, monkeypatch):
        await client.call_tool("team_create", {"team_name": "tsg3"})
        teams.add_member("tsg3", _make_teammate("worker", "tsg3"))
        result = await client.call_tool(
            "process_shutdown_approved",
            {"team_name": "tsg3", "agent_name": "worker"},
        )
        assert result.is_error is False

    async def test_should_remove_member_on_shutdown_window(
        self, client: Client, monkeypatch
    ):
        await client.call_tool("team_create", {"team_name": "tsg4"})
        teams.add_member("tsg4", _make_teammate("worker", "tsg4"))
        result = await client.call_tool(
            "process_shutdown_approved",
            {"team_name": "tsg4", "agent_name": "worker"},
        )
        assert result.is_error is False


class TestErrorWrapping:
    async def test_read_config_wraps_file_not_found(self, client: Client):
        result = await client.call_tool(
            "read_config",
            {"team_name": "nonexistent"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "not found" in result.content[0].text.lower()

    async def test_send_message_wraps_missing_team(self, client: Client):
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "nonexistent",
                "type": "message",
                "recipient": "bob",
                "content": "hi",
                "summary": "test",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "not found" in result.content[0].text.lower()
        assert "Traceback" not in result.content[0].text

    async def test_task_get_wraps_file_not_found(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tew"})
        result = await client.call_tool(
            "task_get",
            {"team_name": "tew", "task_id": "999"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "not found" in result.content[0].text.lower()

    async def test_task_update_wraps_file_not_found(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tew2"})
        result = await client.call_tool(
            "task_update",
            {"team_name": "tew2", "task_id": "999", "status": "completed"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "not found" in result.content[0].text.lower()

    async def test_task_create_wraps_nonexistent_team(self, client: Client):
        result = await client.call_tool(
            "task_create",
            {"team_name": "ghost-team", "subject": "x", "description": "y"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "does not exist" in result.content[0].text.lower()

    async def test_task_update_wraps_validation_error(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tew3"})
        created = _data(
            await client.call_tool(
                "task_create",
                {"team_name": "tew3", "subject": "S", "description": "d"},
            )
        )
        await client.call_tool(
            "task_update",
            {"team_name": "tew3", "task_id": created["id"], "status": "in_progress"},
        )
        result = await client.call_tool(
            "task_update",
            {"team_name": "tew3", "task_id": created["id"], "status": "pending"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "cannot transition" in result.content[0].text.lower()

    async def test_task_list_wraps_nonexistent_team(self, client: Client):
        result = await client.call_tool(
            "task_list",
            {"team_name": "ghost-team"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "does not exist" in result.content[0].text.lower()


class TestTeamDeleteErrorWrapping:
    async def test_should_reject_delete_with_active_members(self, client: Client):
        await client.call_tool("team_create", {"team_name": "td1"})
        teams.add_member("td1", _make_teammate("worker", "td1"))
        result = await client.call_tool(
            "team_delete",
            {"team_name": "td1"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "member" in result.content[0].text.lower()

    async def test_should_reject_delete_nonexistent_team(self, client: Client):
        result = await client.call_tool(
            "team_delete",
            {"team_name": "ghost-team"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "Traceback" not in result.content[0].text


class TestPlanApprovalValidation:
    async def test_should_reject_plan_approval_to_nonexistent_recipient(
        self, client: Client
    ):
        await client.call_tool("team_create", {"team_name": "tp1"})
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tp1",
                "type": "plan_approval_response",
                "recipient": "ghost",
                "approve": True,
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "ghost" in result.content[0].text

    async def test_should_reject_plan_approval_with_empty_recipient(
        self, client: Client
    ):
        await client.call_tool("team_create", {"team_name": "tp2"})
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tp2",
                "type": "plan_approval_response",
                "recipient": "",
                "approve": True,
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "recipient" in result.content[0].text.lower()


@pytest.fixture
async def opencode_client(tmp_path: Path, monkeypatch):
    # 此 fixture 已过时（opencode backend 已移除），保留以避免收集错误
    yield None


@pytest.fixture
async def opencode_only_client(tmp_path: Path, monkeypatch):
    # 此 fixture 已过时（opencode backend 已移除），保留以避免收集错误
    yield None


@pytest.mark.skip(reason="_build_spawn_description 已在任务 5 中移除")
class TestBuildSpawnDescription:
    def test_should_reference_tmux_pane_by_default(self, monkeypatch) -> None:
        pytest.skip("_build_spawn_description 已在任务 5 中移除")

    def test_should_reference_tmux_window_when_enabled(self, monkeypatch) -> None:
        pytest.skip("_build_spawn_description 已在任务 5 中移除")

    def test_both_backends_available(self) -> None:
        pytest.skip("_build_spawn_description 已在任务 5 中移除")

    def test_only_claude_available(self) -> None:
        pytest.skip("_build_spawn_description 已在任务 5 中移除")
        assert "'opencode'" not in desc

    def test_only_opencode_available(self) -> None:
        desc = _build_spawn_description(
            None,
            "/bin/opencode",
            ["model-x"],
            opencode_server_url="http://localhost:4096",
        )
        assert "'claude'" not in desc
        assert "'opencode'" in desc
        assert "model-x" in desc

    def test_opencode_with_no_models(self) -> None:
        desc = _build_spawn_description(
            "/bin/claude",
            "/bin/opencode",
            [],
            opencode_server_url="http://localhost:4096",
        )
        assert "none discovered" in desc

    def test_opencode_hidden_when_server_url_missing(self) -> None:
        desc = _build_spawn_description("/bin/claude", "/bin/opencode", ["model-a"])
        assert "'opencode'" not in desc
        assert "'claude'" in desc

    def test_should_include_agent_names_and_descriptions(self) -> None:
        agents = [
            {"name": "build", "description": "The default agent."},
            {"name": "explore", "description": "Fast explorer."},
        ]
        desc = _build_spawn_description(
            "/bin/claude",
            "/bin/opencode",
            ["model-a"],
            opencode_server_url="http://localhost:4096",
            opencode_agents=agents,
        )
        assert "build: The default agent." in desc
        assert "explore: Fast explorer." in desc
        assert "subagent_type" in desc

    def test_should_omit_agents_section_when_empty(self) -> None:
        desc = _build_spawn_description(
            "/bin/claude",
            "/bin/opencode",
            ["model-a"],
            opencode_server_url="http://localhost:4096",
            opencode_agents=[],
        )
        assert "opencode agents" not in desc.lower()

    def test_should_hide_opencode_when_not_in_enabled_backends(
        self, monkeypatch
    ) -> None:
        monkeypatch.delenv("USE_TMUX_WINDOWS", raising=False)
        desc = _build_spawn_description(
            "/bin/claude",
            "/bin/opencode",
            ["model-a"],
            opencode_server_url="http://localhost:4096",
            enabled_backends=["claude"],
        )
        assert "'claude'" in desc
        assert "'opencode'" not in desc

    def test_should_hide_claude_when_not_in_enabled_backends(self, monkeypatch) -> None:
        monkeypatch.delenv("USE_TMUX_WINDOWS", raising=False)
        desc = _build_spawn_description(
            "/bin/claude",
            "/bin/opencode",
            ["model-a"],
            opencode_server_url="http://localhost:4096",
            enabled_backends=["opencode"],
        )
        assert "'claude'" not in desc
        assert "'opencode'" in desc

    def test_should_show_both_when_both_enabled(self, monkeypatch) -> None:
        monkeypatch.delenv("USE_TMUX_WINDOWS", raising=False)
        desc = _build_spawn_description(
            "/bin/claude",
            "/bin/opencode",
            ["model-a"],
            opencode_server_url="http://localhost:4096",
            enabled_backends=["claude", "opencode"],
        )
        assert "'claude'" in desc
        assert "'opencode'" in desc

    def test_should_hide_opencode_agents_when_opencode_not_enabled(
        self, monkeypatch
    ) -> None:
        monkeypatch.delenv("USE_TMUX_WINDOWS", raising=False)
        agents = [{"name": "build", "description": "The default agent."}]
        desc = _build_spawn_description(
            "/bin/claude",
            "/bin/opencode",
            ["model-a"],
            opencode_server_url="http://localhost:4096",
            opencode_agents=agents,
            enabled_backends=["claude"],
        )
        assert "build" not in desc
        assert "opencode agents" not in desc.lower()


@pytest.mark.skip(reason="backend_type 参数已在任务 3 中移除")
class TestSpawnBackendType:
    async def test_should_reject_opencode_when_binary_not_found(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tbt1"})
        result = await client.call_tool(
            "spawn_teammate",
            {
                "team_name": "tbt1",
                "name": "worker",
                "cwd": "/tmp",
                "backend_type": "opencode",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "opencode" in result.content[0].text.lower()

    async def test_should_spawn_opencode_teammate_successfully(
        self, opencode_client: Client
    ):
        await opencode_client.call_tool("team_create", {"team_name": "tbt2"})
        result = await opencode_client.call_tool(
            "spawn_teammate",
            {
                "team_name": "tbt2",
                "name": "oc-worker",
                "cwd": "/tmp",
                "backend_type": "opencode",
                "model": "anthropic/claude-opus-4-6",
            },
        )
        data = _data(result)
        assert data["name"] == "oc-worker"
        assert data["agent_id"] == "oc-worker@tbt2"

    async def test_should_reject_claude_when_binary_not_found(
        self, opencode_only_client: Client
    ):
        await opencode_only_client.call_tool("team_create", {"team_name": "tbt3"})
        result = await opencode_only_client.call_tool(
            "spawn_teammate",
            {
                "team_name": "tbt3",
                "name": "worker",
                "cwd": "/tmp",
                "backend_type": "claude",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "claude" in result.content[0].text.lower()


class TestShutdownOpencodeTeammate:
    async def test_shutdown_approved_includes_acpx_session_name(
        self, client: Client
    ):
        await client.call_tool("team_create", {"team_name": "tsd1"})
        teams.add_member("tsd1", _make_teammate("oc-worker", "tsd1"))
        await client.call_tool(
            "send_message",
            {
                "team_name": "tsd1",
                "type": "shutdown_response",
                "sender": "oc-worker",
                "request_id": "req-oc-1",
                "approve": True,
            },
        )
        inbox = _data(
            await client.call_tool(
                "read_inbox", {"team_name": "tsd1", "agent_name": "team-lead"}
            )
        )
        assert len(inbox) == 1
        payload = json.loads(inbox[0]["text"])
        assert payload["type"] == "shutdown_approved"
        assert payload["acpxSessionName"] == "oc-worker@tsd1"
        assert payload["from"] == "oc-worker"


@pytest.fixture
async def claude_only_env_client(tmp_path: Path, monkeypatch):
    # 此 fixture 已过时（backend_type 逻辑已移除），保留以避免收集错误
    yield None


@pytest.mark.skip(reason="CLAUDE_TEAMS_BACKENDS/backend_type 逻辑已在任务 3/5 中移除")
class TestEnabledBackendsValidation:
    async def test_should_reject_disabled_backend_on_spawn(
        self, claude_only_env_client: Client
    ):
        await claude_only_env_client.call_tool("team_create", {"team_name": "teb1"})
        result = await claude_only_env_client.call_tool(
            "spawn_teammate",
            {
                "team_name": "teb1",
                "name": "worker",
                "cwd": "/tmp",
                "backend_type": "opencode",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "not enabled" in result.content[0].text.lower()

    async def test_should_accept_enabled_backend_on_spawn(
        self, claude_only_env_client: Client
    ):
        await claude_only_env_client.call_tool("team_create", {"team_name": "teb2"})
        result = await claude_only_env_client.call_tool(
            "spawn_teammate",
            {
                "team_name": "teb2",
                "name": "worker",
                "cwd": "/tmp",
                "backend_type": "claude",
            },
        )
        assert result.is_error is False


@pytest.fixture
async def opencode_env_no_url_client(tmp_path: Path, monkeypatch):
    # 此 fixture 已过时（opencode backend 已移除），保留以避免收集错误
    yield None


@pytest.mark.skip(reason="opencode backend 已在任务 3/5 中移除")
class TestOpencodeWithoutUrl:
    async def test_should_exclude_opencode_when_url_missing(
        self, opencode_env_no_url_client: Client
    ):
        await opencode_env_no_url_client.call_tool("team_create", {"team_name": "tnou"})
        result = await opencode_env_no_url_client.call_tool(
            "spawn_teammate",
            {
                "team_name": "tnou",
                "name": "worker",
                "cwd": "/tmp",
                "backend_type": "opencode",
            },
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "not enabled" in result.content[0].text.lower()


class TestCheckTeammate:
    async def test_should_return_error_for_unknown_agent(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tct_unk"})
        result = await client.call_tool(
            "check_teammate",
            {"team_name": "tct_unk", "agent_name": "ghost"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "ghost" in result.content[0].text

    async def test_should_return_error_for_missing_team(self, client: Client):
        result = await client.call_tool(
            "check_teammate",
            {"team_name": "nonexistent", "agent_name": "bob"},
            raise_on_error=False,
        )
        assert result.is_error is True
        assert "not found" in result.content[0].text.lower()

    async def test_should_return_not_alive_for_empty_pane_id(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tct_empty"})
        teams.add_member("tct_empty", _make_teammate("worker", "tct_empty"))
        result = _data(
            await client.call_tool(
                "check_teammate",
                {"team_name": "tct_empty", "agent_name": "worker"},
            )
        )
        assert result["alive"] is False
        assert result["name"] == "worker"

    async def test_should_return_alive_true_when_acpx_returns_alive(
        self, client: Client, monkeypatch
    ):
        from claude_teams import acpx_client as _acpx
        monkeypatch.setattr(
            _acpx, "get_session_status",
            lambda agent, session_name, cwd, acpx_binary="acpx", timeout=30: "alive"
        )
        await client.call_tool("team_create", {"team_name": "tct_live"})
        teams.add_member("tct_live", _make_teammate("worker", "tct_live"))
        result = _data(
            await client.call_tool(
                "check_teammate",
                {"team_name": "tct_live", "agent_name": "worker"},
            )
        )
        assert result["alive"] is True
        assert result["name"] == "worker"

    async def test_should_not_include_output_by_default(
        self, client: Client, monkeypatch
    ):
        # output 字段已在 acpx 迁移后移除，验证不存在该字段
        await client.call_tool("team_create", {"team_name": "tct_noout"})
        teams.add_member("tct_noout", _make_teammate("worker", "tct_noout"))
        result = _data(
            await client.call_tool(
                "check_teammate",
                {"team_name": "tct_noout", "agent_name": "worker"},
            )
        )
        assert "output" not in result

    async def test_should_return_pending_from_messages(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tct_msg"})
        teams.add_member("tct_msg", _make_teammate("worker", "tct_msg"))
        # Worker sends a message to team-lead
        await client.call_tool(
            "send_message",
            {
                "team_name": "tct_msg",
                "type": "message",
                "sender": "worker",
                "recipient": "team-lead",
                "content": "task done",
                "summary": "status",
            },
        )
        result = _data(
            await client.call_tool(
                "check_teammate",
                {"team_name": "tct_msg", "agent_name": "worker"},
            )
        )
        assert len(result["pending_from"]) == 1
        assert result["pending_from"][0]["from"] == "worker"

    async def test_should_mark_pending_from_as_read(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tct_read"})
        teams.add_member("tct_read", _make_teammate("worker", "tct_read"))
        await client.call_tool(
            "send_message",
            {
                "team_name": "tct_read",
                "type": "message",
                "sender": "worker",
                "recipient": "team-lead",
                "content": "done",
                "summary": "s",
            },
        )
        # First check returns the message
        result1 = _data(
            await client.call_tool(
                "check_teammate",
                {"team_name": "tct_read", "agent_name": "worker"},
            )
        )
        assert len(result1["pending_from"]) == 1
        # Second check returns empty (already read)
        result2 = _data(
            await client.call_tool(
                "check_teammate",
                {"team_name": "tct_read", "agent_name": "worker"},
            )
        )
        assert len(result2["pending_from"]) == 0

    async def test_should_not_mark_other_senders_as_read(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tct_sel"})
        teams.add_member("tct_sel", _make_teammate("alice", "tct_sel"))
        teams.add_member("tct_sel", _make_teammate("bob", "tct_sel"))
        # Both send messages to team-lead
        for sender in ("alice", "bob"):
            await client.call_tool(
                "send_message",
                {
                    "team_name": "tct_sel",
                    "type": "message",
                    "sender": sender,
                    "recipient": "team-lead",
                    "content": f"from {sender}",
                    "summary": "s",
                },
            )
        # Check alice only
        result = _data(
            await client.call_tool(
                "check_teammate",
                {"team_name": "tct_sel", "agent_name": "alice"},
            )
        )
        assert len(result["pending_from"]) == 1
        assert result["pending_from"][0]["from"] == "alice"
        # Bob's message should still be unread
        inbox = _data(
            await client.call_tool(
                "read_inbox",
                {"team_name": "tct_sel", "agent_name": "team-lead"},
            )
        )
        # Should have bob's message (alice's was already read by check_teammate)
        bob_msgs = [m for m in inbox if m["from"] == "bob"]
        assert len(bob_msgs) == 1

    async def test_should_include_their_unread_count(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tct_uc"})
        teams.add_member("tct_uc", _make_teammate("worker", "tct_uc"))
        # Send a message TO the worker (so their inbox has unread)
        await client.call_tool(
            "send_message",
            {
                "team_name": "tct_uc",
                "type": "message",
                "recipient": "worker",
                "content": "do this",
                "summary": "task",
            },
        )
        result = _data(
            await client.call_tool(
                "check_teammate",
                {"team_name": "tct_uc", "agent_name": "worker"},
            )
        )
        assert result["their_unread_count"] == 1

    async def test_should_include_push_available_field(self, client: Client):
        await client.call_tool("team_create", {"team_name": "tct_push"})
        teams.add_member("tct_push", _make_teammate("worker", "tct_push"))
        result = _data(
            await client.call_tool(
                "check_teammate",
                {"team_name": "tct_push", "agent_name": "worker"},
            )
        )
        assert "push_available" in result
        assert "notification_scheduled" in result

    @pytest.mark.skip(reason="notify_after_minutes 参数已在任务 5 中移除")
    async def test_should_reject_notify_after_minutes_below_one(self, client: Client):
        pass

    @pytest.mark.skip(reason="notify_after_minutes/opencode push 已在任务 5 中移除")
    async def test_should_schedule_notification_when_push_available(
        self, client: Client, monkeypatch
    ):
        pass

    @pytest.mark.skip(reason="notify_after_minutes 参数已在任务 5 中移除")
    async def test_should_error_when_notify_requested_but_push_unavailable(
        self, client: Client
    ):
        pass

    async def test_should_skip_messages_when_include_messages_false(
        self, client: Client
    ):
        await client.call_tool("team_create", {"team_name": "tct_nomsg"})
        teams.add_member("tct_nomsg", _make_teammate("worker", "tct_nomsg"))
        await client.call_tool(
            "send_message",
            {
                "team_name": "tct_nomsg",
                "type": "message",
                "sender": "worker",
                "recipient": "team-lead",
                "content": "hello",
                "summary": "s",
            },
        )
        result = _data(
            await client.call_tool(
                "check_teammate",
                {
                    "team_name": "tct_nomsg",
                    "agent_name": "worker",
                    "include_messages": False,
                },
            )
        )
        assert result["pending_from"] == []
        # Message should still be unread in lead inbox
        inbox = _data(
            await client.call_tool(
                "read_inbox",
                {"team_name": "tct_nomsg", "agent_name": "team-lead"},
            )
        )
        assert len(inbox) == 1


class TestCheckTeammateDescription:
    async def test_should_indicate_push_unavailable_by_default(self, client: Client):
        """Default description (no opencode session) should warn against notify_after_minutes."""
        tools = await client.list_tools()
        check_tool = next(t for t in tools if t.name == "check_teammate")
        assert "NOT available" in check_tool.description
        assert "Do NOT pass notify_after_minutes" in check_tool.description

    async def test_should_indicate_push_available_when_session_known(
        self, client: Client, monkeypatch
    ):
        """When lead session is discovered, description should advertise push."""
        from claude_teams.server import (
            _build_check_teammate_description,
            _check_teammate_tool,
        )

        # Simulate what middleware does
        if _check_teammate_tool:
            _check_teammate_tool.description = _build_check_teammate_description(
                push_available=True
            )
        try:
            tools = await client.list_tools()
            check_tool = next(t for t in tools if t.name == "check_teammate")
            assert "available in this session" in check_tool.description
            assert "notify_after_minutes" in check_tool.description
            assert "NOT available" not in check_tool.description
        finally:
            # Restore
            if _check_teammate_tool:
                _check_teammate_tool.description = _build_check_teammate_description(
                    push_available=False
                )


@pytest.mark.skip(reason="_parse_backends_env 已在任务 5 中移除（backend_type 逻辑已删除）")
class TestEnabledBackendsEnvParsing:
    def test_should_parse_comma_separated_backends(self):
        pass

    def test_should_handle_whitespace_in_backends(self):
        pass

    def test_should_ignore_empty_segments(self):
        pass

    def test_should_return_empty_for_blank_string(self):
        pass

    def test_should_filter_out_unknown_backends(self):
        pass

    def test_should_filter_all_unknown_backends(self):
        pass


class TestReadInboxDescription:
    async def test_should_show_default_description_without_lead_session(
        self, client: Client
    ):
        tools = await client.list_tools()
        ri_tool = next(t for t in tools if t.name == "read_inbox")
        assert "Returns unread messages by default" in ri_tool.description
        assert "check_teammate" not in ri_tool.description

    async def test_should_recommend_check_teammate_for_lead(self, client: Client):
        from claude_teams.server import _read_inbox_tool

        if _read_inbox_tool:
            _read_inbox_tool.description = _build_read_inbox_description(
                is_lead_session=True
            )
        try:
            tools = await client.list_tools()
            ri_tool = next(t for t in tools if t.name == "read_inbox")
            assert "check_teammate" in ri_tool.description
            assert "prefer" in ri_tool.description.lower()
        finally:
            if _read_inbox_tool:
                _read_inbox_tool.description = _build_read_inbox_description(
                    is_lead_session=False
                )


# ============================================================
# 子任务 5.1：属性测试 - check_teammate 状态映射
# Feature: acpx-backend-migration, Property 4: check_teammate 状态映射
# 验证：需求 8.2
# ============================================================

from hypothesis import given, settings
import hypothesis.strategies as st
from claude_teams.acpx_client import AcpxError


class TestCheckTeammateStatusMappingProperty:
    """
    属性 4：check_teammate 状态映射
    对任意状态字符串，验证 "alive" 映射为 alive=True，
    "dead" 或 AcpxError 映射为 alive=False。
    **Validates: Requirements 8.2**
    """

    @given(status=st.just("alive"))
    @settings(max_examples=50)
    def test_alive_status_maps_to_true(self, status: str):
        """属性：get_session_status 返回 "alive" 时，alive 应为 True"""
        # 直接测试映射逻辑（不通过 MCP 工具层）
        alive = status == "alive"
        assert alive is True

    @given(status=st.text(min_size=1).filter(lambda s: s != "alive"))
    @settings(max_examples=100)
    def test_non_alive_status_maps_to_false(self, status: str):
        """属性：get_session_status 返回非 "alive" 字符串时，alive 应为 False"""
        alive = status == "alive"
        assert alive is False

    def test_acpx_error_maps_to_false(self):
        """属性：get_session_status 抛出 AcpxError 时，alive 应为 False"""
        # 模拟 AcpxError 场景下的映射逻辑
        alive = True
        try:
            raise AcpxError("session not found")
        except AcpxError:
            alive = False
        assert alive is False

    @given(
        status=st.sampled_from(["dead", "stopped", "error", "unknown", "terminated"])
    )
    @settings(max_examples=50)
    def test_dead_like_statuses_map_to_false(self, status: str):
        """属性：各种非 alive 状态均映射为 False"""
        alive = status == "alive"
        assert alive is False


# ============================================================
# 子任务 5.2：单元测试 - force_kill_teammate、process_shutdown_approved、send_message
# 验证：需求 5.6、5.7、5.10
# ============================================================

class TestForceKillTeammateCallsCloseSession:
    """测试 force_kill_teammate 调用 acpx_client.close_session"""

    async def test_should_call_close_session_on_force_kill(
        self, client: Client, monkeypatch
    ):
        from claude_teams import acpx_client as _acpx
        closed = []
        monkeypatch.setattr(
            _acpx, "close_session",
            lambda agent, session_name, cwd, acpx_binary="acpx", timeout=30: closed.append(session_name)
        )
        await client.call_tool("team_create", {"team_name": "tfk1"})
        teams.add_member("tfk1", _make_teammate("worker", "tfk1"))
        result = await client.call_tool(
            "force_kill_teammate",
            {"team_name": "tfk1", "agent_name": "worker"},
        )
        assert result.is_error is False
        assert "worker@tfk1" in closed

    async def test_should_succeed_even_if_close_session_raises(
        self, client: Client, monkeypatch
    ):
        """close_session 失败时应记录警告但不抛出异常（best-effort）"""
        from claude_teams import acpx_client as _acpx
        monkeypatch.setattr(
            _acpx, "close_session",
            lambda agent, session_name, cwd, acpx_binary="acpx", timeout=30: (_ for _ in ()).throw(AcpxError("failed"))
        )
        await client.call_tool("team_create", {"team_name": "tfk2"})
        teams.add_member("tfk2", _make_teammate("worker", "tfk2"))
        result = await client.call_tool(
            "force_kill_teammate",
            {"team_name": "tfk2", "agent_name": "worker"},
        )
        assert result.is_error is False


class TestProcessShutdownApprovedCallsCloseSession:
    """测试 process_shutdown_approved 调用 acpx_client.close_session"""

    async def test_should_call_close_session_on_shutdown(
        self, client: Client, monkeypatch
    ):
        from claude_teams import acpx_client as _acpx
        closed = []
        monkeypatch.setattr(
            _acpx, "close_session",
            lambda agent, session_name, cwd, acpx_binary="acpx", timeout=30: closed.append(session_name)
        )
        await client.call_tool("team_create", {"team_name": "tpsa1"})
        teams.add_member("tpsa1", _make_teammate("worker", "tpsa1"))
        result = await client.call_tool(
            "process_shutdown_approved",
            {"team_name": "tpsa1", "agent_name": "worker"},
        )
        assert result.is_error is False
        assert "worker@tpsa1" in closed

    async def test_should_succeed_even_if_close_session_raises(
        self, client: Client, monkeypatch
    ):
        """close_session 失败时应记录警告但不抛出异常（best-effort）"""
        from claude_teams import acpx_client as _acpx
        monkeypatch.setattr(
            _acpx, "close_session",
            lambda agent, session_name, cwd, acpx_binary="acpx", timeout=30: (_ for _ in ()).throw(AcpxError("failed"))
        )
        await client.call_tool("team_create", {"team_name": "tpsa2"})
        teams.add_member("tpsa2", _make_teammate("worker", "tpsa2"))
        result = await client.call_tool(
            "process_shutdown_approved",
            {"team_name": "tpsa2", "agent_name": "worker"},
        )
        assert result.is_error is False


class TestSendMessageNoOpencodeCall:
    """测试 send_message 不再调用任何 opencode 相关函数"""

    async def test_message_does_not_call_opencode(self, client: Client, monkeypatch):
        """send_message 不应调用任何 opencode push 函数"""
        opencode_calls = []

        # 确保没有 opencode_client 属性可被调用
        import claude_teams.server as server_mod
        assert not hasattr(server_mod, "opencode_client"), \
            "server.py 不应再导入 opencode_client"

        await client.call_tool("team_create", {"team_name": "tsm1"})
        teams.add_member("tsm1", _make_teammate("bob", "tsm1"))
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tsm1",
                "type": "message",
                "recipient": "bob",
                "content": "hello",
                "summary": "greet",
            },
        )
        assert result.is_error is False
        assert opencode_calls == []

    async def test_broadcast_does_not_call_opencode(self, client: Client, monkeypatch):
        """broadcast 不应调用任何 opencode push 函数"""
        import claude_teams.server as server_mod
        assert not hasattr(server_mod, "opencode_client"), \
            "server.py 不应再导入 opencode_client"

        await client.call_tool("team_create", {"team_name": "tsm2"})
        teams.add_member("tsm2", _make_teammate("bob", "tsm2"))
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tsm2",
                "type": "broadcast",
                "content": "hello all",
                "summary": "announce",
            },
        )
        assert result.is_error is False

    async def test_shutdown_request_does_not_call_opencode(
        self, client: Client, monkeypatch
    ):
        """shutdown_request 不应调用任何 opencode push 函数"""
        import claude_teams.server as server_mod
        assert not hasattr(server_mod, "opencode_client"), \
            "server.py 不应再导入 opencode_client"

        await client.call_tool("team_create", {"team_name": "tsm3"})
        teams.add_member("tsm3", _make_teammate("bob", "tsm3"))
        result = await client.call_tool(
            "send_message",
            {
                "team_name": "tsm3",
                "type": "shutdown_request",
                "recipient": "bob",
                "content": "please stop",
            },
        )
        assert result.is_error is False
