# =============================================================================
# 企微IT智能服务台 — 摇人多坐席协作功能 测试
# =============================================================================
# 测试覆盖：
#   一、邀请协作（POST /api/conversations/{id}/invite）
#     1. 成功邀请：collaborating_agent_ids 更新，WS广播，WS定向推送
#     2. 邀请已结单会话 → 3002
#     3. 邀请未接单会话（queued）→ 3020
#     4. 非主责/协作坐席邀请 → 3021
#     5. 邀请主责坐席本人 → 3022
#     6. 邀请已在协作中的坐席 → 3023
#     7. 邀请离线坐席 → 3024
#     8. 协作坐席也可以摇人（再摇第三人）
#     9. 邀请不存在的坐席 → 3004
#     10. 邀请不存在的会话 → 3003
#
#   二、退出协作（POST /api/conversations/{id}/leave）
#     1. 成功退出：从 collaborating_agent_ids 移除，WS 广播
#     2. 主责坐席尝试退出 → 3025
#     3. 非协作坐席退出 → 3026
#     4. 退出后清空当前选中会话
#
#   三、列表集成测试
#     1. collaborating_agent_ids 字段正确
#     2. collaborating_agent_names 姓名映射正确
#     3. is_collaborator 字段正确
#     4. 协作坐席仍能查看和回复
#
#   四、权限矩阵验证（端到端）
#     1. 协作坐席不能结单
#     2. 协作坐席不能转接
#     3. 协作坐席不占负载
# =============================================================================

import uuid
from datetime import datetime
from unittest.mock import AsyncMock, MagicMock, patch

import pytest
import pytest_asyncio
from httpx import ASGITransport, AsyncClient
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession

from app.models.agent import Agent
from app.models.conversation import Conversation
from app.services.session_service import SessionService
from app.utils.response import AppException
from tests.conftest import create_test_conversation, create_test_agent, MockRedis


# =============================================================================
# 辅助函数
# =============================================================================

async def login_agent(client: AsyncClient, user_id: str, name: str) -> dict:
    """登录坐席并返回认证头字典。"""
    response = await client.post(
        "/agents/login",
        json={"user_id": user_id, "name": name},
    )
    data = response.json()
    token = data["data"]["token"]
    return {"Authorization": f"Bearer {token}"}


async def create_serving_conversation(
    db_session: AsyncSession,
    employee_id: str = "emp_001",
    agent_user_id: str = "agent_owner",
    collab_ids: list = None,
) -> Conversation:
    """创建一个 serving 状态且有主责坐席的会话（可选已有协作坐席）。"""
    conv = create_test_conversation(
        employee_id=employee_id,
        status="serving",
    )
    conv.assigned_agent_id = agent_user_id
    conv.collaborating_agent_ids = collab_ids or []
    db_session.add(conv)
    await db_session.flush()
    return conv


# =============================================================================
# 一、邀请协作测试
# =============================================================================

class TestInviteCollaborator:
    """测试邀请协作接口 POST /api/conversations/{id}/invite。"""

    @pytest.mark.asyncio
    async def test_invite_success_updates_collaborating_ids(
        self, client, db_session, mock_redis
    ):
        """验证成功邀请：collaborating_agent_ids 更新，WS广播+定向推送。

        场景：坐席A（owner）在处理会话，邀请在线坐席B加入协作。
        """
        # 创建坐席
        owner = create_test_agent(user_id="owner_001", name="坐席A", status="online")
        invitee = create_test_agent(user_id="invitee_001", name="坐席B", status="online")
        db_session.add_all([owner, invitee])
        await db_session.flush()

        # 创建 serving 会话，分配给 owner
        conv = await create_serving_conversation(
            db_session, employee_id="emp_invite", agent_user_id="owner_001"
        )

        # 坐席A 登录并发起邀请
        headers = await login_agent(client, "owner_001", "坐席A")

        with patch("app.services.ws_manager.manager.broadcast", new_callable=AsyncMock) as mock_broadcast, \
             patch("app.services.ws_manager.manager.send_to_agent", new_callable=AsyncMock) as mock_send:
            response = await client.post(
                f"/conversations/{conv.id}/invite",
                json={"agent_id": "invitee_001"},
                headers=headers,
            )

        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0

        # 验证 collaborating_agent_ids 包含被邀请坐席
        result = data["data"]
        assert "invitee_001" in result["collaborating_agent_ids"]

        # 验证 WS 广播被调用（collaborator_joined）
        mock_broadcast.assert_called_once()
        broadcast_msg = mock_broadcast.call_args[0][0]
        assert broadcast_msg["type"] == "collaborator_joined"
        assert broadcast_msg["data"]["agent_id"] == "invitee_001"
        assert broadcast_msg["data"]["inviter_agent_id"] == "owner_001"

        # 验证 WS 定向推送被调用（collaborator_invited）
        mock_send.assert_called_once_with("invitee_001", mock_send.call_args[0][1])
        sent_msg = mock_send.call_args[0][1]
        assert sent_msg["type"] == "collaborator_invited"
        assert sent_msg["data"]["invitee_agent_id"] == "invitee_001"

        # 验证数据库持久化
        stmt = select(Conversation).where(Conversation.id == conv.id)
        result_db = await db_session.execute(stmt)
        db_conv = result_db.scalars().first()
        assert "invitee_001" in db_conv.collaborating_agent_ids

    @pytest.mark.asyncio
    async def test_invite_resolved_conversation_error_3002(
        self, client, db_session, mock_redis
    ):
        """验证不能邀请已结单会话 → 3002。"""
        owner = create_test_agent(user_id="owner_resolved", name="坐席A", status="online")
        invitee = create_test_agent(user_id="invitee_resolved", name="坐席B", status="online")
        db_session.add_all([owner, invitee])
        await db_session.flush()

        conv = create_test_conversation(
            employee_id="emp_resolved_invite", status="resolved"
        )
        conv.assigned_agent_id = "owner_resolved"
        db_session.add(conv)
        await db_session.flush()

        headers = await login_agent(client, "owner_resolved", "坐席A")
        response = await client.post(
            f"/conversations/{conv.id}/invite",
            json={"agent_id": "invitee_resolved"},
            headers=headers,
        )

        data = response.json()
        assert data["code"] == 3002  # ERR_CONVERSATION_RESOLVED

    @pytest.mark.asyncio
    async def test_invite_queued_conversation_error_3020(
        self, client, db_session, mock_redis
    ):
        """验证不能邀请未接单（queued）的会话 → 3020。"""
        owner = create_test_agent(user_id="owner_queued", name="坐席A", status="online")
        invitee = create_test_agent(user_id="invitee_queued", name="坐席B", status="online")
        db_session.add_all([owner, invitee])
        await db_session.flush()

        # queued 状态，未分配坐席 → 应先报 3021（不是 owner/collaborator）
        # 但如果有 assigned_agent_id=owner，则是 queued 状态 → 3020
        conv = create_test_conversation(
            employee_id="emp_queued_invite", status="queued"
        )
        conv.assigned_agent_id = "owner_queued"
        db_session.add(conv)
        await db_session.flush()

        headers = await login_agent(client, "owner_queued", "坐席A")
        response = await client.post(
            f"/conversations/{conv.id}/invite",
            json={"agent_id": "invitee_queued"},
            headers=headers,
        )

        data = response.json()
        assert data["code"] == 3020
        assert "服务中" in data["message"]

    @pytest.mark.asyncio
    async def test_invite_by_non_owner_error_3021(
        self, client, db_session, mock_redis
    ):
        """验证非主责/协作坐席不能摇人 → 3021。"""
        owner = create_test_agent(user_id="owner_3021", name="坐席A", status="online")
        invitee = create_test_agent(user_id="invitee_3021", name="坐席B", status="online")
        stranger = create_test_agent(user_id="stranger_3021", name="路过的坐席", status="online")
        db_session.add_all([owner, invitee, stranger])
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session, employee_id="emp_3021", agent_user_id="owner_3021"
        )

        # 用路过的坐席登录（既不是主责也不是协作坐席）
        headers = await login_agent(client, "stranger_3021", "路过的坐席")
        response = await client.post(
            f"/conversations/{conv.id}/invite",
            json={"agent_id": "invitee_3021"},
            headers=headers,
        )

        data = response.json()
        assert data["code"] == 3021
        assert "摇人" in data["message"]

    @pytest.mark.asyncio
    async def test_invite_owner_self_error_3022(
        self, client, db_session, mock_redis
    ):
        """验证不能邀请主责坐席本人 → 3022。"""
        owner = create_test_agent(user_id="owner_3022", name="坐席A", status="online")
        db_session.add(owner)
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session, employee_id="emp_3022", agent_user_id="owner_3022"
        )

        headers = await login_agent(client, "owner_3022", "坐席A")
        response = await client.post(
            f"/conversations/{conv.id}/invite",
            json={"agent_id": "owner_3022"},  # 邀请自己
            headers=headers,
        )

        data = response.json()
        assert data["code"] == 3022

    @pytest.mark.asyncio
    async def test_invite_duplicate_collaborator_error_3023(
        self, client, db_session, mock_redis
    ):
        """验证不能重复邀请已在协作中的坐席 → 3023。"""
        owner = create_test_agent(user_id="owner_3023", name="坐席A", status="online")
        invitee = create_test_agent(user_id="invitee_3023", name="坐席B", status="online")
        db_session.add_all([owner, invitee])
        await db_session.flush()

        # 坐席B 已在协作列表中
        conv = await create_serving_conversation(
            db_session,
            employee_id="emp_3023",
            agent_user_id="owner_3023",
            collab_ids=["invitee_3023"],
        )

        headers = await login_agent(client, "owner_3023", "坐席A")
        response = await client.post(
            f"/conversations/{conv.id}/invite",
            json={"agent_id": "invitee_3023"},
            headers=headers,
        )

        data = response.json()
        assert data["code"] == 3023

    @pytest.mark.asyncio
    async def test_invite_offline_agent_error_3024(
        self, client, db_session, mock_redis
    ):
        """验证不能邀请离线坐席 → 3024。"""
        owner = create_test_agent(user_id="owner_3024", name="坐席A", status="online")
        offline_agent = create_test_agent(user_id="offline_3024", name="离线坐席", status="offline")
        db_session.add_all([owner, offline_agent])
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session, employee_id="emp_3024", agent_user_id="owner_3024"
        )

        headers = await login_agent(client, "owner_3024", "坐席A")
        response = await client.post(
            f"/conversations/{conv.id}/invite",
            json={"agent_id": "offline_3024"},
            headers=headers,
        )

        data = response.json()
        assert data["code"] == 3024
        assert "不在线" in data["message"]

    @pytest.mark.asyncio
    async def test_invite_nonexistent_agent_error_3004(
        self, client, db_session, mock_redis
    ):
        """验证邀请不存在的坐席 → 3004。"""
        owner = create_test_agent(user_id="owner_3004", name="坐席A", status="online")
        db_session.add(owner)
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session, employee_id="emp_3004", agent_user_id="owner_3004"
        )

        headers = await login_agent(client, "owner_3004", "坐席A")
        response = await client.post(
            f"/conversations/{conv.id}/invite",
            json={"agent_id": "nonexistent_agent"},
            headers=headers,
        )

        data = response.json()
        assert data["code"] == 3004  # ERR_AGENT_NOT_FOUND

    @pytest.mark.asyncio
    async def test_invite_nonexistent_conversation_error_3003(
        self, client, db_session, mock_redis
    ):
        """验证邀请不存在的会话 → 3003。"""
        agent = create_test_agent(user_id="agent_3003", name="坐席A", status="online")
        invitee = create_test_agent(user_id="invitee_3003", name="坐席B", status="online")
        db_session.add_all([agent, invitee])
        await db_session.flush()

        fake_id = str(uuid.uuid4())
        headers = await login_agent(client, "agent_3003", "坐席A")
        response = await client.post(
            f"/conversations/{fake_id}/invite",
            json={"agent_id": "invitee_3003"},
            headers=headers,
        )

        data = response.json()
        assert data["code"] == 3003  # ERR_CONVERSATION_NOT_FOUND

    @pytest.mark.asyncio
    async def test_collaborator_can_also_invite_others(
        self, client, db_session, mock_redis
    ):
        """验证协作坐席也可以摇人（再摇第三人加入）。

        场景：坐席A（owner）邀请坐席B → 坐席B 再摇坐席C
        """
        owner = create_test_agent(user_id="chain_owner", name="坐席A", status="online")
        collab1 = create_test_agent(user_id="chain_collab1", name="坐席B", status="online")
        collab2 = create_test_agent(user_id="chain_collab2", name="坐席C", status="online")
        db_session.add_all([owner, collab1, collab2])
        await db_session.flush()

        # 坐席A 创建会话，邀请坐席B
        conv = await create_serving_conversation(
            db_session,
            employee_id="emp_chain",
            agent_user_id="chain_owner",
            collab_ids=["chain_collab1"],
        )

        # 坐席B 登录并发起邀请坐席C
        headers = await login_agent(client, "chain_collab1", "坐席B")

        with patch("app.services.ws_manager.manager.broadcast", new_callable=AsyncMock), \
             patch("app.services.ws_manager.manager.send_to_agent", new_callable=AsyncMock):
            response = await client.post(
                f"/conversations/{conv.id}/invite",
                json={"agent_id": "chain_collab2"},
                headers=headers,
            )

        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0

        # 验证 collaborating_agent_ids 包含两个协作坐席
        result = data["data"]
        assert "chain_collab1" in result["collaborating_agent_ids"]
        assert "chain_collab2" in result["collaborating_agent_ids"]

    @pytest.mark.asyncio
    async def test_invite_without_auth_returns_unauthorized(
        self, client, db_session, mock_redis
    ):
        """验证未登录时邀请返回未授权错误。"""
        owner = create_test_agent(user_id="noauth_owner", name="坐席A", status="online")
        invitee = create_test_agent(user_id="noauth_invitee", name="坐席B", status="online")
        db_session.add_all([owner, invitee])
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session, employee_id="emp_noauth", agent_user_id="noauth_owner"
        )

        response = await client.post(
            f"/conversations/{conv.id}/invite",
            json={"agent_id": "noauth_invitee"},
        )

        data = response.json()
        assert data["code"] == 1002  # ERR_UNAUTHORIZED


# =============================================================================
# 二、退出协作测试
# =============================================================================

class TestLeaveCollaboration:
    """测试退出协作接口 POST /api/conversations/{id}/leave。"""

    @pytest.mark.asyncio
    async def test_leave_success_removes_from_list(
        self, client, db_session, mock_redis
    ):
        """验证成功退出：从 collaborating_agent_ids 移除，WS 广播。"""
        owner = create_test_agent(user_id="leave_owner", name="坐席A", status="online")
        collab = create_test_agent(user_id="leave_collab", name="坐席B", status="online")
        db_session.add_all([owner, collab])
        await db_session.flush()

        # 坐席B 已在协作列表中
        conv = await create_serving_conversation(
            db_session,
            employee_id="emp_leave",
            agent_user_id="leave_owner",
            collab_ids=["leave_collab"],
        )

        headers = await login_agent(client, "leave_collab", "坐席B")

        with patch("app.services.ws_manager.manager.broadcast", new_callable=AsyncMock) as mock_broadcast:
            response = await client.post(
                f"/conversations/{conv.id}/leave",
                headers=headers,
            )

        assert response.status_code == 200
        data = response.json()
        assert data["code"] == 0

        # 验证 collaborating_agent_ids 不再包含坐席B
        result = data["data"]
        assert "leave_collab" not in result["collaborating_agent_ids"]

        # 验证 WS 广播被调用（collaborator_left）
        mock_broadcast.assert_called_once()
        broadcast_msg = mock_broadcast.call_args[0][0]
        assert broadcast_msg["type"] == "collaborator_left"
        assert broadcast_msg["data"]["agent_id"] == "leave_collab"

        # 验证数据库持久化
        stmt = select(Conversation).where(Conversation.id == conv.id)
        result_db = await db_session.execute(stmt)
        db_conv = result_db.scalars().first()
        assert "leave_collab" not in db_conv.collaborating_agent_ids

    @pytest.mark.asyncio
    async def test_leave_owner_error_3025(
        self, client, db_session, mock_redis
    ):
        """验证主责坐席不能退出协作 → 3025。"""
        owner = create_test_agent(user_id="leave_owner_3025", name="坐席A", status="online")
        db_session.add(owner)
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session,
            employee_id="emp_leave_owner",
            agent_user_id="leave_owner_3025",
            collab_ids=["some_collab"],
        )

        headers = await login_agent(client, "leave_owner_3025", "坐席A")
        response = await client.post(
            f"/conversations/{conv.id}/leave",
            headers=headers,
        )

        data = response.json()
        assert data["code"] == 3025
        assert "主责坐席" in data["message"]

    @pytest.mark.asyncio
    async def test_leave_non_collaborator_error_3026(
        self, client, db_session, mock_redis
    ):
        """验证不在协作列表中的坐席不能退出 → 3026。"""
        owner = create_test_agent(user_id="leave_owner_3026", name="坐席A", status="online")
        stranger = create_test_agent(user_id="stranger_3026", name="路过的坐席", status="online")
        db_session.add_all([owner, stranger])
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session,
            employee_id="emp_leave_stranger",
            agent_user_id="leave_owner_3026",
        )

        headers = await login_agent(client, "stranger_3026", "路过的坐席")
        response = await client.post(
            f"/conversations/{conv.id}/leave",
            headers=headers,
        )

        data = response.json()
        assert data["code"] == 3026
        assert "协作列表" in data["message"]

    @pytest.mark.asyncio
    async def test_leave_without_auth_returns_unauthorized(
        self, client, db_session, mock_redis
    ):
        """验证未登录时退出返回未授权错误。"""
        owner = create_test_agent(user_id="noauth_leave_owner", name="坐席A", status="online")
        collab = create_test_agent(user_id="noauth_leave_collab", name="坐席B", status="online")
        db_session.add_all([owner, collab])
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session,
            employee_id="emp_noauth_leave",
            agent_user_id="noauth_leave_owner",
            collab_ids=["noauth_leave_collab"],
        )

        response = await client.post(
            f"/conversations/{conv.id}/leave",
        )

        data = response.json()
        assert data["code"] == 1002  # ERR_UNAUTHORIZED


# =============================================================================
# 三、列表集成测试
# =============================================================================

class TestCollaborationListIntegration:
    """测试会话列表接口的协作字段集成。"""

    @pytest.mark.asyncio
    async def test_list_includes_collaboration_fields(
        self, client, db_session, mock_redis
    ):
        """验证列表接口返回 collaborating_agent_ids 和 _names 字段。"""
        owner = create_test_agent(user_id="list_owner", name="坐席A", status="online")
        collab1 = create_test_agent(user_id="list_collab1", name="坐席B", status="online")
        collab2 = create_test_agent(user_id="list_collab2", name="坐席C", status="online")
        db_session.add_all([owner, collab1, collab2])
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session,
            employee_id="emp_list_collab",
            agent_user_id="list_owner",
            collab_ids=["list_collab1", "list_collab2"],
        )

        # 以坐席A身份查看列表
        headers = await login_agent(client, "list_owner", "坐席A")
        response = await client.get("/conversations", headers=headers)

        data = response.json()
        assert data["code"] == 0
        items = data["data"]["items"]
        item_map = {item["id"]: item for item in items}

        conv_item = item_map[str(conv.id)]

        # 验证 collaborating_agent_ids
        assert "list_collab1" in conv_item["collaborating_agent_ids"]
        assert "list_collab2" in conv_item["collaborating_agent_ids"]
        assert len(conv_item["collaborating_agent_ids"]) == 2

        # 验证 collaborating_agent_names
        assert conv_item["collaborating_agent_names"]["list_collab1"] == "坐席B"
        assert conv_item["collaborating_agent_names"]["list_collab2"] == "坐席C"

        # 验证 is_collaborator（坐席A是主责不是协作坐席）
        assert conv_item["is_collaborator"] is False

    @pytest.mark.asyncio
    async def test_list_is_collaborator_field_correctness(
        self, client, db_session, mock_redis
    ):
        """验证 is_collaborator 字段标注正确。

        - 主责坐席 → is_collaborator=False
        - 协作坐席（且非主责）→ is_collaborator=True
        - 既非主责也非协作 → is_collaborator=False
        """
        owner = create_test_agent(user_id="iscoll_owner", name="主责坐席", status="online")
        collab = create_test_agent(user_id="iscoll_collab", name="协作坐席", status="online")
        stranger = create_test_agent(user_id="iscoll_stranger", name="路人坐席", status="online")
        db_session.add_all([owner, collab, stranger])
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session,
            employee_id="emp_iscoll",
            agent_user_id="iscoll_owner",
            collab_ids=["iscoll_collab"],
        )

        # 主责坐席查看 → is_collaborator=False
        headers_owner = await login_agent(client, "iscoll_owner", "主责坐席")
        resp = await client.get("/conversations", headers=headers_owner)
        items = resp.json()["data"]["items"]
        item_map = {item["id"]: item for item in items}
        assert item_map[str(conv.id)]["is_collaborator"] is False

        # 协作坐席查看 → is_collaborator=True
        headers_collab = await login_agent(client, "iscoll_collab", "协作坐席")
        resp = await client.get("/conversations", headers=headers_collab)
        items = resp.json()["data"]["items"]
        item_map = {item["id"]: item for item in items}
        assert item_map[str(conv.id)]["is_collaborator"] is True

        # 路人坐席查看 → is_collaborator=False
        headers_stranger = await login_agent(client, "iscoll_stranger", "路人坐席")
        resp = await client.get("/conversations", headers=headers_stranger)
        items = resp.json()["data"]["items"]
        item_map = {item["id"]: item for item in items}
        assert item_map[str(conv.id)]["is_collaborator"] is False

    @pytest.mark.asyncio
    async def test_list_no_collaborators_returns_empty_arrays(
        self, client, db_session, mock_redis
    ):
        """验证无协作坐席时返回空数组和空对象。"""
        owner = create_test_agent(user_id="empty_owner", name="坐席A", status="online")
        db_session.add(owner)
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session, employee_id="emp_empty_collab", agent_user_id="empty_owner"
        )

        headers = await login_agent(client, "empty_owner", "坐席A")
        response = await client.get("/conversations", headers=headers)

        data = response.json()
        items = data["data"]["items"]
        item_map = {item["id"]: item for item in items}

        conv_item = item_map[str(conv.id)]
        assert conv_item["collaborating_agent_ids"] == []
        assert conv_item["collaborating_agent_names"] == {}
        assert conv_item["is_collaborator"] is False


# =============================================================================
# 四、权限矩阵验证（端到端）
# =============================================================================

class TestCollaborationPermissions:
    """测试协作坐席的权限边界。

    协作坐席可以：查看会话、发送回复、摇人（再邀请）
    协作坐席不能：结单、转接、置顶/代办
    协作坐席不占负载。
    """

    @pytest.mark.asyncio
    async def test_collaborator_does_not_count_load(
        self, client, db_session, mock_redis
    ):
        """验证协作坐席加入后负载不变（不占负载）。

        主责坐席 current_load 应保持为1，协作坐席 current_load 保持不变。
        """
        owner = create_test_agent(user_id="load_owner", name="坐席A", status="online")
        owner.current_load = 1

        collab = create_test_agent(user_id="load_collab", name="坐席B", status="online")
        collab.current_load = 0

        db_session.add_all([owner, collab])
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session, employee_id="emp_load", agent_user_id="load_owner"
        )

        headers = await login_agent(client, "load_owner", "坐席A")

        with patch("app.services.ws_manager.manager.broadcast", new_callable=AsyncMock), \
             patch("app.services.ws_manager.manager.send_to_agent", new_callable=AsyncMock):
            await client.post(
                f"/conversations/{conv.id}/invite",
                json={"agent_id": "load_collab"},
                headers=headers,
            )

        # 验证主责坐席 load 不变（=1）
        stmt = select(Agent).where(Agent.user_id == "load_owner")
        result = await db_session.execute(stmt)
        db_owner = result.scalars().first()
        assert db_owner.current_load == 1

        # 验证协作坐席 load 不变（=0）
        stmt = select(Agent).where(Agent.user_id == "load_collab")
        result = await db_session.execute(stmt)
        db_collab = result.scalars().first()
        assert db_collab.current_load == 0  # 协作不占负载

    @pytest.mark.asyncio
    async def test_collaborator_cannot_resolve_conversation(
        self, client, db_session, mock_redis
    ):
        """验证协作坐席不能结单。

        场景：坐席A（owner）邀请坐席B协作 → 坐席B 尝试结单（应失败）
        """
        owner = create_test_agent(user_id="perm_owner", name="坐席A", status="online")
        collab = create_test_agent(user_id="perm_collab", name="坐席B", status="online")
        db_session.add_all([owner, collab])
        await db_session.flush()

        conv = await create_serving_conversation(
            db_session,
            employee_id="emp_perm",
            agent_user_id="perm_owner",
            collab_ids=["perm_collab"],
        )

        # 协作坐席登录并尝试结单
        headers = await login_agent(client, "perm_collab", "坐席B")

        with patch("app.services.ws_manager.manager.broadcast", new_callable=AsyncMock):
            response = await client.post(
                f"/conversations/{conv.id}/resolve",
                headers=headers,
            )

        data = response.json()
        # 协作坐席不是主责坐席，resolve 应返回 3027（只有主责坐席才能结单）
        assert data["code"] == 3027, f"协作坐席不应该能结单，期望 code=3027，实际 code={data['code']}"

    @pytest.mark.asyncio
    async def test_full_invite_leave_cycle(
        self, client, db_session, mock_redis
    ):
        """端到端测试：邀请→查看列表→退出→验证清理。

        完整流程：
        1. 坐席A 邀请坐席B
        2. 坐席B 查看列表，确认协作会话出现
        3. 坐席A 邀请坐席C
        4. 坐席A 查看列表，验证两个协作坐席
        5. 坐席B 退出协作
        6. 验证坐席B 不再出现在协作列表中，坐席C 仍存在
        """
        # 创建坐席
        owner = create_test_agent(user_id="e2e_owner", name="坐席A", status="online")
        collab_b = create_test_agent(user_id="e2e_b", name="坐席B", status="online")
        collab_c = create_test_agent(user_id="e2e_c", name="坐席C", status="online")
        db_session.add_all([owner, collab_b, collab_c])
        await db_session.flush()

        # 创建会话
        conv = await create_serving_conversation(
            db_session, employee_id="emp_e2e", agent_user_id="e2e_owner"
        )

        headers_a = await login_agent(client, "e2e_owner", "坐席A")

        # Step 1: 坐席A 邀请坐席B
        with patch("app.services.ws_manager.manager.broadcast", new_callable=AsyncMock), \
             patch("app.services.ws_manager.manager.send_to_agent", new_callable=AsyncMock):
            resp = await client.post(
                f"/conversations/{conv.id}/invite",
                json={"agent_id": "e2e_b"},
                headers=headers_a,
            )
        assert resp.json()["code"] == 0

        # Step 2: 坐席B 查看列表，确认协作会话出现
        headers_b = await login_agent(client, "e2e_b", "坐席B")
        resp = await client.get("/conversations", headers=headers_b)
        items = resp.json()["data"]["items"]
        item_map = {item["id"]: item for item in items}
        assert str(conv.id) in item_map
        assert item_map[str(conv.id)]["is_collaborator"] is True

        # Step 3: 坐席A 邀请坐席C
        with patch("app.services.ws_manager.manager.broadcast", new_callable=AsyncMock), \
             patch("app.services.ws_manager.manager.send_to_agent", new_callable=AsyncMock):
            resp = await client.post(
                f"/conversations/{conv.id}/invite",
                json={"agent_id": "e2e_c"},
                headers=headers_a,
            )
        assert resp.json()["code"] == 0

        # Step 4: 坐席A 查看列表，验证两个协作坐席
        resp = await client.get("/conversations", headers=headers_a)
        items = resp.json()["data"]["items"]
        item_map = {item["id"]: item for item in items}
        conv_item = item_map[str(conv.id)]
        assert "e2e_b" in conv_item["collaborating_agent_ids"]
        assert "e2e_c" in conv_item["collaborating_agent_ids"]
        assert conv_item["collaborating_agent_names"]["e2e_b"] == "坐席B"
        assert conv_item["collaborating_agent_names"]["e2e_c"] == "坐席C"

        # Step 5: 坐席B 退出协作
        with patch("app.services.ws_manager.manager.broadcast", new_callable=AsyncMock):
            resp = await client.post(
                f"/conversations/{conv.id}/leave",
                headers=headers_b,
            )
        assert resp.json()["code"] == 0

        # Step 6: 验证坐席B 已移除，坐席C 仍存在
        resp = await client.get("/conversations", headers=headers_a)
        items = resp.json()["data"]["items"]
        item_map = {item["id"]: item for item in items}
        conv_item = item_map[str(conv.id)]
        assert "e2e_b" not in conv_item["collaborating_agent_ids"]
        assert "e2e_c" in conv_item["collaborating_agent_ids"]
