#!/usr/bin/env python3
"""
微信全功能 E2E 端到端测试脚本 — 96个action / 29个功能模块

验证完整链路: 网页/API → unified.py → 通道路由 → Agent(Hook/u2) 或 ADB引擎 → 微信APP

前置条件:
1. SDK 运行: localhost:8899 (cd sdk/app && python3 main.py)
2. 设备连接: USB ADB 或 Agent WebSocket
3. 微信已登录
4. 安全联系人: 文件传输助手 (每台微信必有)

用法:
  python3 test_wechat_full_e2e.py                     # 运行安全测试组(只读/文件传输助手)
  python3 test_wechat_full_e2e.py --full               # 运行全量测试(含写操作)
  python3 test_wechat_full_e2e.py --group message      # 只运行消息组
  python3 test_wechat_full_e2e.py --action send_message # 只运行单个action

环境变量:
  SDK_BASE_URL:   SDK地址 (默认 http://localhost:8899)
  SDK_DEVICE_ID:  设备ID (默认自动检测)
  SDK_E2E_TO_ID:  测试联系人 (默认 文件传输助手)
"""

import httpx
import asyncio
import json
import time
import os
import sys
import argparse
from typing import Dict, Any, List, Tuple
from dataclasses import dataclass, field
from datetime import datetime

BASE_URL = os.environ.get("SDK_BASE_URL", "http://localhost:8899")
DEVICE_ID = os.environ.get("SDK_DEVICE_ID", "")
TO_ID = os.environ.get("SDK_E2E_TO_ID", "文件传输助手")
TIMEOUT = 60


@dataclass
class CheckResult:
    action: str
    group: str
    channel: str
    success: bool
    elapsed_ms: int
    response: dict
    error: str = ""


@dataclass
class CheckReport:
    results: List[CheckResult] = field(default_factory=list)
    start_time: float = 0
    end_time: float = 0

    @property
    def total(self): return len(self.results)
    @property
    def passed(self): return sum(1 for r in self.results if r.success)
    @property
    def failed(self): return self.total - self.passed
    @property
    def success_rate(self): return f"{self.passed/self.total*100:.1f}%" if self.total else "N/A"
    @property
    def avg_time(self): return sum(r.elapsed_ms for r in self.results) // max(self.total, 1)

    def print_summary(self):
        elapsed = self.end_time - self.start_time
        print("\n" + "=" * 70)
        print(f"  微信全功能 E2E 测试报告")
        print(f"  {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        print("=" * 70)
        print(f"  总计: {self.total}  通过: {self.passed}  失败: {self.failed}  成功率: {self.success_rate}")
        print(f"  总耗时: {elapsed:.1f}s  平均单操作: {self.avg_time}ms")
        print("-" * 70)

        groups = {}
        for r in self.results:
            groups.setdefault(r.group, []).append(r)

        for grp, items in groups.items():
            ok = sum(1 for i in items if i.success)
            print(f"\n  [{grp}] {ok}/{len(items)} 通过")
            for r in items:
                icon = "✓" if r.success else "✗"
                ch = f"[{r.channel}]" if r.channel else ""
                err = f" — {r.error[:60]}" if r.error else ""
                print(f"    {icon} {r.action:<30} {r.elapsed_ms:>5}ms {ch:<12}{err}")

        print("\n" + "=" * 70)

        if self.failed:
            print("\n  失败详情:")
            for r in self.results:
                if not r.success:
                    print(f"\n  [{r.group}] {r.action}")
                    print(f"    错误: {r.error}")
                    print(f"    响应: {json.dumps(r.response, ensure_ascii=False)[:200]}")


TS = int(time.time())

ALL_TESTS: Dict[str, List[Tuple[str, str, str, dict, bool]]] = {
    "消息": [
        ("send_message",    "POST", "/api/v3/message/send",
         {"to_id": TO_ID, "content": f"[E2E] {datetime.now()}", "msg_type": "text"}, True),
        ("get_messages",    "POST", "/api/v3/message/list",
         {"conversation_id": TO_ID, "limit": 5}, True),
        ("forward_message", "POST", "/api/v3/message/forward",
         {"to_id": TO_ID, "content": "转发测试"}, True),
        ("recall_message",  "POST", "/api/v3/message/recall", {}, True),
        ("send_card",       "POST", "/api/v3/message/send-card",
         {"to_id": TO_ID, "card_wxid": TO_ID}, True),
    ],
    "好友": [
        ("get_contacts",    "GET",  "/api/v3/contacts", {"limit": 10}, True),
        ("search_contact",  "GET",  "/api/v3/contacts/search", {"keyword": "文件"}, True),
        ("friend_info",     "GET",  "/api/v3/friend/info", {"user_id": TO_ID}, True),
    ],
    "群聊": [
        ("get_groups",      "GET",  "/api/v3/group/list", {"limit": 10}, True),
        ("get_group_members","GET", "/api/v3/group/members", {"group_id": ""}, True),
    ],
    "标签": [
        ("get_tags",        "GET",  "/api/v3/tag/list", {}, True),
    ],
    "朋友圈": [
        ("get_moments",     "POST", "/api/v3/moments/list", {"limit": 5}, True),
    ],
    "个人": [
        ("get_profile",     "GET",  "/api/v3/profile/get", {}, True),
    ],
    "安全": [
        ("account_status",  "GET",  "/api/v3/account/status", {}, True),
        ("safety_center",   "GET",  "/api/v3/account/safety-center", {}, True),
        ("restrictions",    "GET",  "/api/v3/account/restrictions", {}, True),
    ],
    "收藏": [
        ("get_favorites",   "GET",  "/api/v3/favorites/list", {"limit": 5}, True),
    ],
    "聊天设置": [
        ("set_chat_top",    "POST", "/api/v3/chat/set-top",
         {"user_id": TO_ID, "enable": True}, True),
        ("set_mute_chat",   "POST", "/api/v3/chat/set-mute",
         {"user_id": TO_ID, "enable": False}, True),
    ],
    "视频号": [
        ("video_list",      "GET",  "/api/v3/video-channel/list", {}, True),
    ],
    "扫一扫": [
        ("my_qr",           "GET",  "/api/v3/scan/my-qr", {}, True),
    ],
    "支付": [
        ("payment_code",    "GET",  "/api/v3/payment/code", {}, True),
        ("wallet",          "GET",  "/api/v3/payment/wallet", {}, True),
        ("transactions",    "GET",  "/api/v3/payment/transactions", {}, True),
    ],
    "搜索": [
        ("wechat_search",   "GET",  "/api/v3/search/wechat", {"keyword": "微信"}, True),
    ],
    "发现": [
        ("top_stories",     "GET",  "/api/v3/discover/top-stories", {}, True),
    ],
    "运动": [
        ("get_steps",       "GET",  "/api/v3/wechat-sport/steps", {}, True),
    ],
    "表情": [
        ("stickers",        "GET",  "/api/v3/emoji/stickers", {}, True),
    ],
    "Hook": [
        ("hook_actions",    "GET",  "/api/v3/hook/actions", {}, True),
    ],
}

WRITE_TESTS: Dict[str, List[Tuple[str, str, str, dict, bool]]] = {
    "消息写": [
        ("batch_send",      "POST", "/api/v3/message/batch-send",
         {"to_ids": [TO_ID], "content": f"[批量E2E] {TS}", "msg_type": "text"}, True),
        ("send_voice",      "POST", "/api/v3/message/voice",
         {"to_id": TO_ID, "duration": 3}, True),
    ],
    "好友写": [
        ("set_remark",      "POST", "/api/v3/friend/set-remark",
         {"user_id": TO_ID, "remark": f"E2E_{TS}"}, True),
    ],
    "标签写": [
        ("create_tag",      "POST", "/api/v3/tag/create",
         {"tag_name": f"e2e_tag_{TS}"}, True),
        ("get_users_by_tag","POST", "/api/v3/tag/users",
         {"tag_name": f"e2e_tag_{TS}"}, True),
    ],
    "个人写": [
        ("set_signature",   "POST", "/api/v3/profile/set-signature",
         {"signature": f"E2E测试 {TS}"}, True),
    ],
    "朋友圈写": [
        ("post_moments",    "POST", "/api/v3/moments/post",
         {"content": f"[E2E测试] {datetime.now().strftime('%H:%M')}"}, True),
        ("like_moments",    "POST", "/api/v3/moments/like",
         {"user_id": TO_ID, "post_index": 0}, True),
        ("comment_moments", "POST", "/api/v3/moments/comment",
         {"user_id": TO_ID, "post_index": 0, "comment": "E2E测试评论"}, True),
        ("set_privacy",     "POST", "/api/v3/moments/set-privacy",
         {"privacy_type": "all"}, True),
    ],
    "聊天写": [
        ("clear_history",   "POST", "/api/v3/chat/clear-history",
         {"user_id": TO_ID}, True),
    ],
    "收藏写": [
        ("add_favorite",    "POST", "/api/v3/favorites/add",
         {"content": "E2E收藏测试", "content_type": "text"}, True),
    ],
    "视频号写": [
        ("like_video",      "POST", "/api/v3/video-channel/like",
         {"index": 0}, True),
    ],
    "运动写": [
        ("like_steps",      "POST", "/api/v3/wechat-sport/like-steps",
         {"user_id": TO_ID}, True),
    ],
    "位置写": [
        ("send_location",   "POST", "/api/v3/location/send",
         {"to_id": TO_ID, "latitude": 31.23, "longitude": 121.47, "name": "测试位置"}, True),
    ],
    "表情写": [
        ("send_emoji",      "POST", "/api/v3/emoji/send",
         {"to_id": TO_ID, "emoji_name": "微笑"}, True),
    ],
    "设置写": [
        ("do_not_disturb",  "POST", "/api/v3/settings/do-not-disturb",
         {"enable": False}, True),
        ("clear_cache",     "POST", "/api/v3/settings/clear-cache", {}, True),
        ("check_update",    "POST", "/api/v3/settings/check-update", {}, True),
    ],
    "小程序写": [
        ("open_miniprogram","POST", "/api/v3/miniprogram/open",
         {"name": "微信支付"}, True),
    ],
    "文件写": [
        ("send_file",       "POST", "/api/v3/file/send",
         {"to_id": TO_ID, "file_path": "/sdcard/test.txt"}, True),
    ],
}

# 矩阵 REST 补全（~97 端点 − 已有 50 action）
MATRIX_REST_TESTS: Dict[str, List[Tuple[str, str, str, dict, bool]]] = {
    "消息补": [
        ("comment_reply", "POST", "/api/v3/comment/reply",
         {"video_id": "1", "comment_id": "1", "content": "probe"}, True),
    ],
    "好友补": [
        ("friend_add", "POST", "/api/v3/friend/add",
         {"user_id": "28533368", "message": "matrix-e2e"}, True),
        ("friend_accept", "POST", "/api/v3/friend/accept", {"user_id": "probe"}, True),
        ("friend_delete", "POST", "/api/v3/friend/delete", {"user_id": "invalid_probe_wxid"}, True),
        ("friend_batch_add", "POST", "/api/v3/friend/batch-add",
         {"user_ids": ["28533368"], "message": "batch-probe"}, True),
    ],
    "群聊补": [
        ("group_create", "POST", "/api/v3/group/create",
         {"group_name": f"e2e_{TS}", "member_ids": [TO_ID, TO_ID]}, True),
        ("group_invite", "POST", "/api/v3/group/invite",
         {"group_id": "", "member_ids": [TO_ID]}, True),
        ("group_remove", "POST", "/api/v3/group/remove",
         {"group_id": "", "member_ids": [TO_ID]}, True),
        ("group_set_notice", "POST", "/api/v3/group/set-notice",
         {"group_id": "", "notice": "e2e notice"}, True),
        ("group_set_name", "POST", "/api/v3/group/set-name",
         {"group_id": "", "group_name": "e2e-group"}, True),
        ("group_send_message", "POST", "/api/v3/group/send-message",
         {"group_id": "", "content": "e2e group msg"}, True),
        ("group_set_welcome", "POST", "/api/v3/group/set-welcome",
         {"group_id": "", "welcome_text": "welcome probe"}, True),
        ("group_quit", "POST", "/api/v3/group/quit", {"group_id": ""}, True),
    ],
    "标签补": [
        ("tag_add", "POST", "/api/v3/tag/add", {"user_id": TO_ID, "tags": [f"e2e_tag_{TS}"]}, True),
        ("tag_remove", "POST", "/api/v3/tag/remove", {"user_id": TO_ID, "tags": [f"e2e_tag_{TS}"]}, True),
        ("tag_delete", "POST", "/api/v3/tag/delete", {"tag_name": f"e2e_tag_{TS}"}, True),
    ],
    "朋友圈补": [
        ("moments_delete", "POST", "/api/v3/moments/delete", {"post_index": 0}, True),
        ("moments_set_cover", "POST", "/api/v3/moments/set-cover", {}, True),
        ("moments_share_link", "POST", "/api/v3/moments/share-link",
         {"content": "https://example.com probe"}, True),
    ],
    "个人补": [
        ("set_nickname", "POST", "/api/v3/profile/set-nickname", {"nickname": "e2e_probe"}, True),
        ("set_avatar", "POST", "/api/v3/profile/set-avatar", {"image_path": "/sdcard/test.jpg"}, True),
        ("set_gender", "POST", "/api/v3/profile/set-gender", {"gender": "1"}, True),
        ("set_region", "POST", "/api/v3/profile/set-region", {"region": "福建 厦门"}, True),
    ],
    "安全补": [
        ("account_unblock", "POST", "/api/v3/account/unblock", {}, True),
        ("change_password", "POST", "/api/v3/account/change-password",
         {"old_pwd": "old", "new_pwd": "new"}, True),
        ("unblock_self", "POST", "/api/v3/account/unblock-self", {}, True),
        ("unblock_appeal", "POST", "/api/v3/account/unblock-appeal", {}, True),
        ("appeal_restriction", "POST", "/api/v3/account/appeal-restriction", {}, True),
        ("unblock_sms", "POST", "/api/v3/account/unblock-sms", {"phone": "13800138000"}, True),
    ],
    "支付补": [
        ("send_red_packet", "POST", "/api/v3/payment/red-packet",
         {"to_id": TO_ID, "amount": "0.01", "message": "probe"}, True),
        ("payment_transfer", "POST", "/api/v3/payment/transfer",
         {"to_id": TO_ID, "amount": "0.01", "description": "probe"}, True),
        ("payment_receive", "POST", "/api/v3/payment/receive", {"amount": "0.01", "desc": "probe"}, True),
        ("receive_red_packet", "POST", "/api/v3/payment/receive-red-packet", {"from_id": TO_ID}, True),
    ],
    "视频号补": [
        ("video_comment", "POST", "/api/v3/video-channel/comment",
         {"index": 0, "comment": "probe"}, True),
        ("video_follow", "POST", "/api/v3/video-channel/follow", {"index": 0}, True),
        ("video_share", "POST", "/api/v3/video-channel/share",
         {"index": 0, "to_id": TO_ID}, True),
    ],
    "扫一扫补": [
        ("scan_qr", "POST", "/api/v3/scan/qr-code", {"qr_content": "probe"}, True),
        ("scan_add_friend", "POST", "/api/v3/scan/add-friend", {"qr_content": "probe"}, True),
        (
            "scan_extract_qr",
            "POST",
            "/api/v3/scan/extract-qr",
            {"image_base64": "iVBORw0KGgo="},
            True,
        ),
        (
            "scan_add_friend_from_image",
            "POST",
            "/api/v3/scan/add-friend-from-image",
            {"image_base64": "iVBORw0KGgo=", "verify_message": "probe"},
            True,
        ),
    ],
    "通话补": [
        ("call_voice", "POST", "/api/v3/call/voice", {"to_id": TO_ID}, True),
        ("call_video", "POST", "/api/v3/call/video", {"to_id": TO_ID}, True),
    ],
    "群发": [
        ("mass_send", "POST", "/api/v3/mass-send",
         {"user_ids": [TO_ID], "content": f"mass {TS}"}, True),
    ],
    "位置补": [
        ("share_realtime_location", "POST", "/api/v3/location/share-realtime",
         {"to_id": TO_ID, "duration_minutes": 5}, True),
    ],
    "文件补": [
        ("file_download", "POST", "/api/v3/file/download", {"user_id": TO_ID}, True),
    ],
    "设置补": [
        ("settings_logout", "POST", "/api/v3/settings/logout", {}, True),
        ("switch_account", "POST", "/api/v3/settings/switch-account", {}, True),
    ],
    "公众号": [
        ("follow_official", "POST", "/api/v3/official-account/follow", {"account_name": "probe"}, True),
    ],
}


async def prefetch_group_id(client: httpx.AsyncClient) -> str:
    """取 chatroom 表存在的群 ID；若无则尝试建群；仍无则返回 probe id（Intent 探针）。"""
    pre = await run_test(client, "get_groups", "GET", "/api/v3/group/list", {"limit": 20}, "预检")
    groups = (pre.response.get("data") or {}).get("groups") or []
    for g in groups:
        gid = g.get("group_id") or g.get("username") or ""
        if not gid:
            continue
        chk = await run_test(client, "get_group_members", "GET", "/api/v3/group/members",
                             {"group_id": gid}, "预检")
        inner = chk.response.get("data") or {}
        if inner.get("members") is not None or inner.get("success"):
            return gid
    self_wxid = ""
    try:
        pr = await run_test(client, "account_status", "GET", "/api/v3/account/status", {}, "预检")
        inner = pr.response.get("data") or {}
        self_wxid = inner.get("wxid") or inner.get("username") or ""
    except Exception:
        pass
    members = ["filehelper"]
    if self_wxid and self_wxid not in members:
        members.append(self_wxid)
    if len(members) < 2:
        members.append("filehelper")
    cr = await run_test(
        client, "group_create", "POST", "/api/v3/group/create",
        {"group_name": f"e2e_{TS}", "member_ids": members[:3]}, "预检",
    )
    data = cr.response.get("data") or {}
    gid = data.get("group_id") or (data.get("group") or {}).get("group_id") or ""
    if gid:
        return gid
    return f"probe_e2e_{TS}@chatroom"


def _inject_group_id(params: dict, group_id: str) -> None:
    if group_id and "group_id" in params and not params.get("group_id"):
        params["group_id"] = group_id


def _device_list_from_api_json(data: dict) -> list:
    """兼容 data 为设备数组或 { devices: [] }"""
    if not isinstance(data, dict):
        return []
    raw = data.get("data")
    if isinstance(raw, list):
        return raw
    if isinstance(raw, dict) and isinstance(raw.get("devices"), list):
        return raw["devices"]
    if isinstance(data.get("devices"), list):
        return data["devices"]
    return []


async def detect_device(client: httpx.AsyncClient) -> str:
    """自动检测在线设备"""
    global DEVICE_ID
    if DEVICE_ID:
        return DEVICE_ID
    try:
        resp = await client.get(f"{BASE_URL}/api/v3/devices", timeout=10)
        devices = _device_list_from_api_json(resp.json())
        if devices:
            DEVICE_ID = devices[0].get("device_id", devices[0].get("serial", str(devices[0])))
            return DEVICE_ID
    except Exception:
        pass
    try:
        resp = await client.get(f"{BASE_URL}/api/v3/adb/devices", timeout=10)
        devices = _device_list_from_api_json(resp.json())
        if devices:
            DEVICE_ID = devices[0].get("serial", devices[0].get("device_id", str(devices[0])))
            return DEVICE_ID
    except Exception:
        pass
    DEVICE_ID = "unknown"
    return DEVICE_ID


async def run_test(client: httpx.AsyncClient, action: str, method: str,
                   endpoint: str, params: dict, group: str) -> CheckResult:
    """执行单个测试（POST 自动兼容 body/query 两种参数模式）"""
    params["device_id"] = DEVICE_ID
    params["platform"] = "wechat"
    start = time.time()
    try:
        url = f"{BASE_URL}{endpoint}"
        if method == "GET":
            qs = "&".join(f"{k}={v}" for k, v in params.items() if v is not None)
            resp = await client.get(f"{url}?{qs}" if qs else url, timeout=TIMEOUT)
        else:
            resp = await client.post(url, json=params, timeout=TIMEOUT)
            if resp.status_code == 422:
                qs = "&".join(f"{k}={v}" for k, v in params.items() if v is not None)
                resp = await client.post(f"{url}?{qs}" if qs else url, timeout=TIMEOUT)

        elapsed = int((time.time() - start) * 1000)
        data = resp.json()
        channel = str(
            data.get("channel_used")
            or data.get("_channel_used")
            or (data.get("data") or {}).get("channel", "unknown")
        )
        ok = resp.status_code == 200 and data.get("code", 200) in (200, 0, None)
        inner = data.get("data") if isinstance(data.get("data"), dict) else {}
        if inner.get("success") is False or data.get("success") is False:
            ok = False
        if "offline" in channel.lower():
            ok = False
        if inner.get("code") == 503:
            ok = False
        error = "" if ok else (data.get("message") or inner.get("message") or data.get("error") or str(data))[:200]
        return CheckResult(action=action, group=group, channel=str(channel),
                          success=ok, elapsed_ms=elapsed, response=data, error=error)
    except Exception as e:
        elapsed = int((time.time() - start) * 1000)
        return CheckResult(action=action, group=group, channel="error",
                          success=False, elapsed_ms=elapsed, response={}, error=str(e)[:100])


async def check_health(client: httpx.AsyncClient) -> bool:
    """检查SDK是否在线"""
    try:
        resp = await client.get(f"{BASE_URL}/health", timeout=10)
        data = resp.json()
        print(f"  SDK状态: {data.get('status', 'unknown')}")
        print(f"  设备在线: {data.get('devices_online', 0)}")
        return True
    except Exception as e:
        print(f"  SDK连接失败: {e}")
        return False


async def main():
    parser = argparse.ArgumentParser(description="微信全功能E2E测试")
    parser.add_argument("--full", action="store_true", help="运行全量测试(含写操作)")
    parser.add_argument("--matrix", action="store_true", help="矩阵 REST 全端点(含 --full + 补全端点)")
    parser.add_argument("--group", type=str, help="只运行指定组")
    parser.add_argument("--action", type=str, help="只运行指定action")
    parser.add_argument("--base-url", type=str, help="SDK地址")
    parser.add_argument("--device-id", type=str, help="设备ID")
    args = parser.parse_args()

    global BASE_URL, DEVICE_ID
    if args.base_url:
        BASE_URL = args.base_url
    if args.device_id:
        DEVICE_ID = args.device_id

    report = CheckReport()
    report.start_time = time.time()

    print("\n" + "=" * 70)
    print("  微信全功能 E2E 端到端测试")
    print("  96个action / 29个功能模块 / 3种控制通道")
    print("=" * 70)

    async with httpx.AsyncClient() as client:
        print("\n[0] 环境检查")
        if not await check_health(client):
            print("\n  请先启动SDK: cd sdk/app && python3 main.py")
            return

        await detect_device(client)
        print(f"  设备ID: {DEVICE_ID}")
        print(f"  测试联系人: {TO_ID}")

        tests = dict(ALL_TESTS)
        if args.full or args.matrix:
            tests.update(WRITE_TESTS)
            os.environ["SDK_MATRIX_VERIFY"] = "1"
        if args.matrix:
            tests.update(MATRIX_REST_TESTS)

        mode = "矩阵 REST 全端点" if args.matrix else ("全量(含写操作)" if args.full else "安全(只读+文件传输助手)")
        print(f"  测试模式: {mode}")

        if args.group:
            tests = {k: v for k, v in tests.items() if k == args.group}
            if not tests:
                print(f"\n  未找到测试组: {args.group}")
                print(f"  可用组: {', '.join(list(ALL_TESTS.keys()) + list(WRITE_TESTS.keys()))}")
                return

        if args.action:
            filtered = {}
            for grp, items in tests.items():
                matching = [t for t in items if t[0] == args.action]
                if matching:
                    filtered[grp] = matching
            tests = filtered
            if not tests:
                print(f"\n  未找到action: {args.action}")
                return

        total_actions = sum(len(v) for v in tests.values())
        print(f"\n  即将测试 {total_actions} 个 action ({len(tests)} 组)")
        print("-" * 70)

        # 预取最近消息 msgId，供 forward/recall 真机链路
        last_msg_id = None
        try:
            pre = await run_test(client, "get_messages", "POST", "/api/v3/message/list",
                                 {"conversation_id": "filehelper", "limit": 3}, "预检")
            msgs = (pre.response.get("data") or {}).get("messages") or []
            if not msgs and isinstance(pre.response.get("data"), dict):
                inner = pre.response["data"].get("data") or pre.response["data"]
                msgs = inner.get("messages") or []
            if msgs:
                last_msg_id = str(msgs[0].get("id") or msgs[0].get("msgId") or msgs[0].get("msg_svr_id") or "")
                print(f"  预检 msg_svr_id={last_msg_id}")
        except Exception as e:
            print(f"  预检 get_messages 跳过: {e}")

        group_id = ""
        if args.matrix:
            group_id = await prefetch_group_id(client)
            print(f"  预检 group_id={group_id or '(空)'}")

        for group_name, group_tests in tests.items():
            print(f"\n[{group_name}] 测试中...")
            for action, method, endpoint, params, safe in group_tests:
                p = dict(params)
                if args.matrix:
                    _inject_group_id(p, group_id)
                if action == "forward_message" and last_msg_id:
                    p["msg_svr_id"] = last_msg_id
                if action == "recall_message" and last_msg_id:
                    p["msg_svr_id"] = last_msg_id
                if action == "friend_info":
                    p["user_id"] = "filehelper"
                result = await run_test(client, action, method, endpoint, p, group_name)
                report.results.append(result)
                icon = "✓" if result.success else "✗"
                print(f"  {icon} {action:<30} {result.elapsed_ms:>5}ms [{result.channel}]")
                if not result.success:
                    print(f"    └ {result.error[:80]}")
                await asyncio.sleep(1.0)

    report.end_time = time.time()
    report.print_summary()

    report_path = os.path.join(os.path.dirname(__file__), "..",
                                "data", "e2e_report.json")
    try:
        os.makedirs(os.path.dirname(report_path), exist_ok=True)
        with open(report_path, "w", encoding="utf-8") as f:
            json.dump({
                "timestamp": datetime.now().isoformat(),
                "device_id": DEVICE_ID,
                "total": report.total,
                "passed": report.passed,
                "failed": report.failed,
                "success_rate": report.success_rate,
                "avg_time_ms": report.avg_time,
                "results": [
                    {
                        "action": r.action, "group": r.group,
                        "channel": r.channel, "success": r.success,
                        "elapsed_ms": r.elapsed_ms, "error": r.error
                    } for r in report.results
                ]
            }, f, ensure_ascii=False, indent=2)
        print(f"\n  报告已保存: {report_path}")
    except Exception as e:
        print(f"\n  保存报告失败: {e}")

    sys.exit(0 if report.failed == 0 else 1)


if __name__ == "__main__":
    asyncio.run(main())
