#!/usr/bin/env python3
"""私域微信能力验收脚本。

默认策略：
- 读类接口直接跑，要求 code=200 且返回结构可计数。
- 高风险写类、资金类默认只验证 dry-run/confirm_required 或参数门禁。
- 真实写入动作只在显式传入白名单参数时执行，避免误触真实客户。
"""

from __future__ import annotations

import argparse
import json
import os
import time
from datetime import datetime
from pathlib import Path
from typing import Any, Callable

import requests


ROOT = Path(__file__).resolve().parents[2]
EVID_ROOT = ROOT / "开发文档" / "8、部署" / "05-测试验收"
REQUEST_TIMEOUT = 45


def post(base: str, path: str, payload: dict[str, Any], timeout: int | None = None) -> dict[str, Any]:
    return requests.post(f"{base}{path}", json=payload, timeout=timeout or REQUEST_TIMEOUT).json()


def get(base: str, path: str, params: dict[str, Any], timeout: int | None = None) -> dict[str, Any]:
    return requests.get(f"{base}{path}", params=params, timeout=timeout or REQUEST_TIMEOUT).json()


def data_of(body: dict[str, Any]) -> dict[str, Any]:
    data = body.get("data")
    return data if isinstance(data, dict) else body


def count_of(data: dict[str, Any], *keys: str) -> int:
    for key in keys:
        val = data.get(key)
        if isinstance(val, list):
            return len(val)
        if isinstance(val, int):
            return val
    return 0


def summarize(body: dict[str, Any]) -> dict[str, Any]:
    data = data_of(body)
    out: dict[str, Any] = {
        "code": body.get("code"),
        "channel_used": body.get("channel_used") or data.get("channel"),
        "success": data.get("success", body.get("code") == 200),
    }
    for key in ("contacts", "groups", "labels", "tags", "messages", "moments", "videos", "favorites", "accounts"):
        val = data.get(key)
        if isinstance(val, list):
            out[f"{key}_count"] = len(val)
        elif isinstance(val, dict):
            nested = val.get(key) or val.get("items") or val.get("list") or val.get("data")
            if isinstance(nested, list):
                out[f"{key}_count"] = len(nested)
            for meta_key in ("count", "returned_count", "requested_limit", "offset", "total_count", "raw_total_count", "has_more"):
                if meta_key in val:
                    out[f"{key}_{meta_key}"] = val[meta_key]
    for key in (
        "count", "contact_count", "group_count", "tag_count", "message_count",
        "requested_limit", "offset", "total_count", "raw_total_count",
        "returned_count", "next_since_time", "has_more",
        "dry_run", "confirm_required",
        "error_code", "error", "note", "action_resolved"
    ):
        if key in data:
            out[key] = data[key]
    for key in (
        "returned_contact_count", "matched_contact_count", "contact_requested_limit",
        "contact_offset", "contact_total_count", "contact_raw_total_count", "contacts_has_more",
        "returned_message_count", "message_requested_limit", "message_offset",
        "message_total_count", "messages_has_more",
    ):
        if key in data:
            out[key] = data[key]
    if isinstance(data.get("summary"), dict):
        out["watch_summary"] = data["summary"]
    if isinstance(data.get("profile"), dict):
        out["profile_present"] = True
    return out


def ok_code(body: dict[str, Any]) -> bool:
    return body.get("code") in (None, 200)


def ok_success_or_gate(body: dict[str, Any]) -> bool:
    data = data_of(body)
    if data.get("success") is True:
        return True
    if data.get("dry_run") is True or data.get("confirm_required") is True:
        return True
    if data.get("error_code") in {"anti_ban_blocked", "validation_required", "missing_whitelist"}:
        return True
    return ok_code(body)


def hook(base: str, device: str, action: str, params: dict[str, Any], hook_only: bool = False) -> dict[str, Any]:
    return post(base, "/api/v3/hook/execute", {
        "device_id": device,
        "platform": "wechat",
        "action": action,
        "params": params,
        "hook_only": hook_only,
    })


def main() -> int:
    global REQUEST_TIMEOUT

    parser = argparse.ArgumentParser()
    parser.add_argument("-d", "--device-id", default=os.getenv("WORKPHONE_DEVICE_ID", "xgfe65eimrrofyws"))
    parser.add_argument("--base", default=os.getenv("WORKPHONE_BASE", "http://127.0.0.1:8899"))
    parser.add_argument("--write-to", default=os.getenv("WORKPHONE_TEST_TO_ID", ""))
    parser.add_argument("--test-user-ids", default=os.getenv("WORKPHONE_TEST_USER_IDS", ""))
    parser.add_argument("--test-group-id", default=os.getenv("WORKPHONE_TEST_GROUP_ID", ""))
    parser.add_argument("--test-sns-id", default=os.getenv("WORKPHONE_TEST_SNS_ID", ""))
    parser.add_argument("--include-real-write", action="store_true")
    parser.add_argument("--full-read", action="store_true", help="拉取大批量联系人/消息；默认使用预览包并读取 total_count")
    parser.add_argument("--request-timeout", type=int, default=int(os.getenv("WORKPHONE_ACCEPTANCE_TIMEOUT", "45")))
    args = parser.parse_args()
    REQUEST_TIMEOUT = max(5, args.request_timeout)

    stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    out_dir = EVID_ROOT / f"{datetime.now().strftime('%Y%m%d')}_微信私域能力验收"
    out_dir.mkdir(parents=True, exist_ok=True)

    device = args.device_id
    base = args.base.rstrip("/")

    rows: list[dict[str, Any]] = []

    def run(name: str, category: str, fn: Callable[[], dict[str, Any]], expect: Callable[[dict[str, Any]], bool] = ok_code):
        started = time.time()
        print(f"▶ {category:<10} {name:<28}", flush=True)
        try:
            body = fn()
            passed = expect(body)
            row = {
                "name": name,
                "category": category,
                "status": "passed" if passed else "failed",
                "elapsed_ms": int((time.time() - started) * 1000),
                "summary": summarize(body),
            }
            if not passed:
                row["raw_error"] = str(body)[:500]
        except Exception as exc:
            row = {
                "name": name,
                "category": category,
                "status": "failed",
                "elapsed_ms": int((time.time() - started) * 1000),
                "error": str(exc),
            }
        rows.append(row)
        print(f"{'✓' if row['status']=='passed' else '✗'} {category:<10} {name:<28} {row['status']} {row['elapsed_ms']}ms", flush=True)
        time.sleep(0.1)

    common = {"device_id": device, "platform": "wechat"}
    contact_limit = 10000 if args.full_read else 500
    message_limit = 3000 if args.full_read else 500

    run("health", "base", lambda: requests.get(f"{base}/health", timeout=30).json())
    run("hook_probe", "base", lambda: requests.get(f"{base}/api/v3/hook/probe/{device}", timeout=60).json())
    run("devices", "base", lambda: get(base, "/api/v3/devices", {}))

    run("profile", "read", lambda: get(base, "/api/v3/profile/get", common))
    run("contacts", "read", lambda: get(base, "/api/v3/contacts", {**common, "limit": contact_limit}))
    run("contacts_page_2", "read", lambda: get(base, "/api/v3/contacts", {**common, "limit": 200, "offset": 200}))
    run("groups", "read", lambda: get(base, "/api/v3/group/list", {**common, "limit": 500}))
    run("tags", "read", lambda: get(base, "/api/v3/tag/list", common))
    run("messages", "read", lambda: post(base, "/api/v3/message/list", {**common, "limit": message_limit}))
    run("messages_page_2", "read", lambda: post(base, "/api/v3/message/list", {**common, "limit": 100, "offset": 100}))
    run("search", "read", lambda: get(base, "/api/v3/search/wechat", {**common, "keyword": "客户"}))
    run("hook_data_preview", "read", lambda: get(base, f"/api/v3/hook/data/{device}", {"modules": "profile,contacts,groups,labels,messages,device_info", "contact_limit": 200, "message_limit": 50, "contact_offset": 0, "message_offset": 0}))
    run("customer_profile_bundle", "read", lambda: get(base, "/api/v3/customer/profile-bundle", {**common, "limit": 10, "contact_limit": 200, "message_limit": 50}))
    run("message_sync_since", "read", lambda: post(base, "/api/v3/message/sync-since", {**common, "since_time": 0, "limit": message_limit}))
    run("favorites", "read", lambda: get(base, "/api/v3/favorites/list", {**common, "limit": 5}), ok_success_or_gate)
    run("official_accounts", "read", lambda: hook(base, device, "get_official_accounts", {"limit": 5}, hook_only=True), ok_success_or_gate)
    run("wallet", "read", lambda: get(base, "/api/v3/payment/wallet", common), ok_success_or_gate)
    run("transactions", "read", lambda: get(base, "/api/v3/payment/transactions", {**common, "limit": 5}), ok_success_or_gate)
    run("stability_watch", "stability", lambda: get(base, "/api/v3/stability/watch", {"device_id": device, "samples": 2, "interval_seconds": 1}))

    run("send_red_packet_dry_run", "gate", lambda: post(base, "/api/v3/payment/red-packet", {
        **common,
        "to_id": args.write_to or "filehelper",
        "amount": "0.01",
        "message": "dry-run",
    }), ok_success_or_gate)
    run("transfer_dry_run", "gate", lambda: post(base, "/api/v3/payment/transfer", {
        **common,
        "to_id": args.write_to or "filehelper",
        "amount": "0.01",
        "message": "dry-run",
    }), ok_success_or_gate)
    run("receive_red_packet_gate", "gate", lambda: hook(base, device, "receive_red_packet", {}, hook_only=False), ok_success_or_gate)

    test_users = [x.strip() for x in args.test_user_ids.split(",") if x.strip()]
    if args.include_real_write and args.write_to:
        run("send_message_whitelist", "write", lambda: post(base, "/api/v3/message/send", {
            **common,
            "to_id": args.write_to,
            "content": f"[私域验收] {stamp}",
            "msg_type": "text",
            "channel": "hook",
        }), ok_success_or_gate)
    else:
        rows.append({"name": "send_message_whitelist", "category": "write", "status": "gated", "reason": "需 --include-real-write + --write-to"})

    if args.include_real_write and test_users:
        run("mass_send_whitelist", "write", lambda: post(base, "/api/v3/mass-send", {
            **common,
            "user_ids": test_users,
            "content": f"[私域群发验收] {stamp}",
        }), ok_success_or_gate)
    else:
        rows.append({"name": "mass_send_whitelist", "category": "write", "status": "gated", "reason": "需 --include-real-write + --test-user-ids"})

    if args.include_real_write and args.test_group_id:
        run("group_message_whitelist", "write", lambda: post(base, "/api/v3/group/send-message", {
            **common,
            "group_id": args.test_group_id,
            "content": f"[私域群消息验收] {stamp}",
        }), ok_success_or_gate)
    else:
        rows.append({"name": "group_message_whitelist", "category": "write", "status": "gated", "reason": "需 --include-real-write + --test-group-id"})

    if args.include_real_write and args.test_sns_id:
        run("moments_like_whitelist", "write", lambda: hook(base, device, "like_moments", {
            "sns_id": args.test_sns_id,
        }, hook_only=False), ok_success_or_gate)
    else:
        rows.append({"name": "moments_like_whitelist", "category": "write", "status": "gated", "reason": "需 --include-real-write + --test-sns-id"})

    passed = sum(1 for r in rows if r["status"] == "passed")
    failed = sum(1 for r in rows if r["status"] == "failed")
    gated = sum(1 for r in rows if r["status"] == "gated")
    report = {
        "timestamp": datetime.now().isoformat(),
        "base": base,
        "device_id": device,
        "summary": {"passed": passed, "failed": failed, "gated": gated, "total": len(rows)},
        "results": rows,
    }
    path = out_dir / f"private_domain_acceptance_{stamp}.json"
    path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"\n私域能力验收: passed={passed} failed={failed} gated={gated} -> {path}")
    return 0 if failed == 0 else 1


if __name__ == "__main__":
    raise SystemExit(main())
