#!/usr/bin/env python3
"""CDP E2E 综合测试 v3 — 完整端到端验证（登录后测试所有功能）"""
import json, time, urllib.request, sys
from websocket import create_connection

# 配置
BASE_URL = "http://localhost:8080/api/v1"
FRONTEND_URL = "http://localhost:5199"
CDP_URL = "http://127.0.0.1:9225"
CREDENTIALS = {"username": "yeij0942", "password": "Jiang1143218570"}

passed = 0
failed = 0

def check(name, condition, detail=""):
    global passed, failed
    if condition:
        passed += 1
        print(f"  ✅ {name}")
    else:
        failed += 1
        print(f"  ❌ {name} {detail}")

# ============================================================
# Part A: Backend API Tests
# ============================================================
print("=" * 60)
print("Part A: Backend API 测试")
print("=" * 60)

# A1: Health checks
print("\n--- A1: 微服务健康检查 ---")
for svc, port in [("gateway", 8080), ("ai-core", 8081), ("memory-service", 8091), ("tool-engine", 8092), ("iot-debug", 8083)]:
    try:
        req = urllib.request.Request(f"http://localhost:{port}/api/v1/health", method="GET")
        resp = urllib.request.urlopen(req, timeout=5)
        check(f"{svc}:{port} 健康检查", resp.status == 200, f"status={resp.status}")
    except Exception as e:
        check(f"{svc}:{port} 健康检查", False, str(e)[:60])

# A2: Login
print("\n--- A2: 用户登录 ---")
try:
    req = urllib.request.Request(
        f"{BASE_URL}/auth/login",
        data=json.dumps(CREDENTIALS).encode(),
        headers={"Content-Type": "application/json"},
        method="POST"
    )
    resp = urllib.request.urlopen(req, timeout=10)
    login_data = json.loads(resp.read())
    access_token = login_data.get("data", {}).get("token", "")
    refresh_token_val = login_data.get("data", {}).get("refresh_token", "")
    user_id = login_data.get("data", {}).get("user_id", "")
    check("登录成功", bool(access_token), f"token={access_token[:20]}...")
    check("获取 refresh_token", bool(refresh_token_val))
    check("获取 user_id", bool(user_id), f"user_id={user_id}")
except Exception as e:
    check("登录", False, str(e)[:80])
    access_token = ""
    refresh_token_val = ""

AUTH_HEADER = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}

# A3: Token Refresh
if refresh_token_val:
    print("\n--- A3: Token 刷新 ---")
    try:
        req = urllib.request.Request(
            f"{BASE_URL}/auth/refresh",
            data=json.dumps({"refresh_token": refresh_token_val}).encode(),
            headers={"Content-Type": "application/json"},
            method="POST"
        )
        resp = urllib.request.urlopen(req, timeout=10)
        refresh_data = json.loads(resp.read())
        new_token = refresh_data.get("data", {}).get("token", "")
        check("Token 刷新成功", bool(new_token), f"new_token={new_token[:20]}...")
        if new_token:
            access_token = new_token
            AUTH_HEADER = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}
    except Exception as e:
        check("Token 刷新", False, str(e)[:80])

# A4: Session CRUD
print("\n--- A4: 会话管理 ---")
session_id = ""
try:
    # Create session
    req = urllib.request.Request(
        f"{BASE_URL}/sessions",
        data=json.dumps({"user_id": user_id, "title": "CDP E2E Test"}).encode(),
        headers=AUTH_HEADER,
        method="POST"
    )
    resp = urllib.request.urlopen(req, timeout=10)
    session_data = json.loads(resp.read())
    session_id = session_data.get("data", {}).get("id", "")
    check("创建会话", bool(session_id), f"session_id={session_id}")

    # List sessions
    req = urllib.request.Request(f"{BASE_URL}/sessions?user_id={user_id}", headers=AUTH_HEADER)
    resp = urllib.request.urlopen(req, timeout=10)
    sessions = json.loads(resp.read())
    check("列出会话", isinstance(sessions.get("data"), list), f"count={len(sessions.get('data', []))}")
except Exception as e:
    check("会话管理", False, str(e)[:80])

# A5: Memory Search
print("\n--- A5: 记忆搜索 ---")
try:
    req = urllib.request.Request(
        f"{BASE_URL}/memory/search",
        data=json.dumps({"user_id": user_id, "query": "test"}).encode(),
        headers=AUTH_HEADER,
        method="POST"
    )
    resp = urllib.request.urlopen(req, timeout=10)
    mem_data = json.loads(resp.read())
    check("记忆搜索API正常", resp.status in [200, 404], f"status={resp.status}")
except Exception as e:
    check("记忆搜索", False, str(e)[:60])

# A6: IoT Devices
print("\n--- A6: IoT 设备 ---")
try:
    req = urllib.request.Request(f"{BASE_URL}/iot/devices", headers=AUTH_HEADER)
    resp = urllib.request.urlopen(req, timeout=10)
    iot_data = json.loads(resp.read())
    devices = iot_data.get("data", {}).get("devices", iot_data.get("data", []))
    check("IoT设备列表API正常", resp.status in [200, 404], f"status={resp.status}, devices={len(devices) if isinstance(devices, list) else 'N/A'}")
except Exception as e:
    check("IoT设备列表", False, str(e)[:60])

print(f"\nBackend 测试结果: {passed} 通过, {failed} 失败")

# ============================================================
# Part B: Frontend CDP E2E Tests
# ============================================================
print("\n" + "=" * 60)
print("Part B: Frontend CDP E2E 测试")
print("=" * 60)

# B1: 创建新的浏览器页面 (about:blank 确保没有 Service Worker 缓存)
print("\n--- B1: 创建新页面 ---")
try:
    req = urllib.request.Request(f"{CDP_URL}/json/new?url=about:blank", method="PUT")
    resp = urllib.request.urlopen(req, timeout=10)
    page = json.loads(resp.read())
    ws_url = page["webSocketDebuggerUrl"]
    page_id = page.get("id", "")
    print(f"  Page ID: {page_id}")
    check("创建空白页面", bool(ws_url))
except Exception as e:
    print(f"  ❌ 创建页面失败: {e}")
    sys.exit(1)

ws = create_connection(ws_url, timeout=15)

def cdp(method, params=None, msg_id=1):
    ws.send(json.dumps({"id": msg_id, "method": method, "params": params or {}}))

def recv_all(timeout=3):
    ws.settimeout(timeout)
    msgs = []
    try:
        while True:
            msgs.append(ws.recv())
    except:
        pass
    return msgs

# Enable domains
cdp("Page.enable", {}, 1)
cdp("Network.enable", {}, 2)
cdp("Runtime.enable", {}, 3)
cdp("Log.enable", {}, 4)
time.sleep(0.5)
recv_all(0.5)

# B2: 导航到前端页面
print("\n--- B2: 导航到前端 ---")
cdp("Page.navigate", {"url": FRONTEND_URL + "/"}, 10)
time.sleep(4)
msgs = recv_all(3)

# 检查 console 日志和错误
has_js_error = False
console_logs = []
for m in msgs:
    d = json.loads(m)
    method = d.get("method", "")
    params = d.get("params", {})
    if method == "Runtime.exceptionThrown":
        has_js_error = True
        exc = params.get("exceptionDetails", {})
        print(f"  [JS ERROR] {exc.get('text', '')}")
    elif method == "Runtime.consoleAPICalled":
        args = params.get("args", [])
        texts = [str(a.get("value", "") or a.get("description", ""))[:120] for a in args]
        console_logs.append(" ".join(texts))
    elif method == "Log.entryAdded":
        entry = params.get("entry", {})
        console_logs.append(f"[{entry.get('level','log')}] {entry.get('text','')[:120]}")

check("页面加载无 JS 异常", not has_js_error)

# B3: 检查 localStorage（应该为空或没有 test-token-cyrene）
print("\n--- B3: localStorage 检查 ---")
cdp("Runtime.evaluate", {
    "expression": "JSON.stringify({token: localStorage.getItem('token'), user_id: localStorage.getItem('user_id'), cyrene_store_version: localStorage.getItem('cyrene_store_version'), allKeys: Object.keys(localStorage)})"
}, 20)
time.sleep(0.5)
msgs = recv_all(1)
ls_data = {}
for m in msgs:
    d = json.loads(m)
    if d.get("id") == 20:
        val = d.get("result", {}).get("result", {}).get("value", "")
        ls_data = json.loads(val) if val else {}
        print(f"  localStorage: {ls_data}")

has_test_token = ls_data.get("token") == "test-token-cyrene"
check("无 test-token-cyrene", not has_test_token, "发现硬编码token!" if has_test_token else "")

# B4: 检查 DOM 渲染（登录页面应出现）
print("\n--- B4: DOM 检查 ---")
cdp("Runtime.evaluate", {
    "expression": "document.querySelector('input') ? 'has_input' : 'no_input'"
}, 30)
time.sleep(0.5)
msgs = recv_all(1)
for m in msgs:
    d = json.loads(m)
    if d.get("id") == 30:
        val = d.get("result", {}).get("result", {}).get("value", "")
        check("登录表单已渲染", val == "'has_input'", f"got: {val}")

# 截图
cdp("Page.captureScreenshot", {"format": "png"}, 31)
time.sleep(1)
msgs = recv_all(2)
for m in msgs:
    d = json.loads(m)
    if d.get("id") == 31:
        img_data = d.get("result", {}).get("data", "")
        if img_data:
            import base64
            with open("/tmp/cdp_screenshot_before_login.png", "wb") as f:
                f.write(base64.b64decode(img_data))
            print("  截图已保存到 /tmp/cdp_screenshot_before_login.png")

# B5: 通过 API 直接在页面上设置 token（绕过登录UI）
print("\n--- B5: 注入 token 并模拟已登录状态 ---")
if access_token:
    cdp("Runtime.evaluate", {
        "expression": f"""
        localStorage.setItem('token', '{access_token}');
        localStorage.setItem('user_id', '{user_id}');
        localStorage.setItem('cyrene_store_version', '1');
        'token_injected'
        """
    }, 40)
    time.sleep(0.5)
    recv_all(0.5)

    # 重新加载页面
    cdp("Page.reload", {}, 41)
    time.sleep(4)
    msgs = recv_all(3)

    # 检查网络请求是否使用正确的 token
    api_calls = []
    console_errors = []
    for m in msgs:
        d = json.loads(m)
        method = d.get("method", "")
        params = d.get("params", {})
        if method == "Network.requestWillBeSent":
            req = params.get("request", {})
            url = req.get("url", "")
            headers = req.get("headers", {})
            auth = headers.get("Authorization", "") or headers.get("authorization", "")
            if "/api/" in url:
                api_calls.append(f"{req.get('method','?')} {url[:100]} Auth={auth[:30]}...")
        elif method == "Runtime.consoleAPICalled":
            args = params.get("args", [])
            if params.get("type") == "error":
                texts = [str(a.get("value", "") or a.get("description", ""))[:150] for a in args]
                console_errors.append(" ".join(texts))
        elif method == "Runtime.exceptionThrown":
            exc = params.get("exceptionDetails", {})
            console_errors.append(f"JS Exception: {exc.get('text', '')}")

    print(f"  API 调用数: {len(api_calls)}")
    for c in api_calls[:5]:
        print(f"    {c}")

    check("API 调用使用正确 token", all("test-token-cyrene" not in c for c in api_calls), "仍使用旧token!" if any("test-token-cyrene" in c for c in api_calls) else "")

    # 检查 WebSocket 连接状态
    ws_connected = any("ws_connect" in c.lower() or "WebSocket" in c for c in console_logs)
    has_auth_error = any("401" in e or "认证" in e or "无效" in e for e in console_errors)
    check("无认证错误", not has_auth_error, str(console_errors[:2]) if console_errors else "")

# 截图 after login
cdp("Page.captureScreenshot", {"format": "png"}, 42)
time.sleep(1)
msgs = recv_all(2)
for m in msgs:
    d = json.loads(m)
    if d.get("id") == 42:
        img_data = d.get("result", {}).get("data", "")
        if img_data:
            import base64
            with open("/tmp/cdp_screenshot_after_login.png", "wb") as f:
                f.write(base64.b64decode(img_data))
            print("  截图已保存到 /tmp/cdp_screenshot_after_login.png")

# B6: DOM 状态检查 - 应该看到侧边栏和聊天界面
print("\n--- B6: 登录后 UI 状态 ---")
checks_js = """
(() => {
    const hasSidebar = !!document.querySelector('aside, [class*="sidebar"], [class*="Sidebar"]');
    const hasChatInput = !!document.querySelector('textarea, [contenteditable="true"], input[type="text"]');
    const hasHeader = !!document.querySelector('header, [class*="header"], [class*="Header"]');
    const bodyText = document.body.innerText.substring(0, 200);
    return JSON.stringify({hasSidebar, hasChatInput, hasHeader, bodyText});
})()
"""
cdp("Runtime.evaluate", {"expression": checks_js}, 50)
time.sleep(0.5)
msgs = recv_all(1)
for m in msgs:
    d = json.loads(m)
    if d.get("id") == 50:
        val = d.get("result", {}).get("result", {}).get("value", "")
        try:
            ui = json.loads(val)
            print(f"  UI状态: sidebar={ui.get('hasSidebar')}, chatInput={ui.get('hasChatInput')}, header={ui.get('hasHeader')}")
            print(f"  Body文本: {ui.get('bodyText', '')[:100]}")
            check("侧边栏已渲染", ui.get("hasSidebar", False))
            check("聊天输入框存在", ui.get("hasChatInput", False))
        except:
            pass

ws.close()

# ============================================================
# Summary
# ============================================================
print("\n" + "=" * 60)
print(f"测试完成: {passed} 通过, {failed} 失败, 总计 {passed+failed}")
print("=" * 60)
