"""
备课模块（lesson-preparation）全功能 Web 测试脚本
使用 Playwright 对所有角色的备课模块访问与功能进行测试

覆盖：
- admin（只读访问 /teacher/lesson-plans）
- teacher（完整 CRUD + 节点编辑器 + 版本管理 + 模板 + 复制 + 归档）
- student（应被拒绝访问 /teacher/lesson-plans）
- parent（应被拒绝访问 /teacher/lesson-plans）

结果输出：webtest/lesson-preparation_v1.md 与 webtest/lesson-preparation_v1.json
"""
import json
import os
import re
import sys
from datetime import datetime
from pathlib import Path
from urllib.parse import urlparse, parse_qs
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout

BASE_URL = "http://localhost:3000"
VERSION = "v1"
MODULE_NAME = "lesson-preparation"

# 测试账号（来自 scripts/seed.ts）
TEST_ACCOUNTS = {
    "admin": {"email": "admin@xiaoxue.edu.cn", "password": "123456", "expected_path": "/admin/dashboard"},
    "teacher": {"email": "t_chinese_1@xiaoxue.edu.cn", "password": "123456", "expected_path": "/teacher/dashboard"},
    "student": {"email": "student_g1c1_1@xiaoxue.edu.cn", "password": "123456", "expected_path": "/student/dashboard"},
    "parent": {"email": "parent_g1c1_1@xiaoxue.edu.cn", "password": "123456", "expected_path": "/parent/dashboard"},
}

# 备课模块路由
LESSON_PLAN_ROUTES = {
    "list": "/teacher/lesson-plans",
    "new": "/teacher/lesson-plans/new",
}

PROJECT_ROOT = Path(__file__).resolve().parents[1]
WEBTEST_DIR = PROJECT_ROOT / "webtest"
SCREENSHOT_DIR = WEBTEST_DIR / "screenshots" / MODULE_NAME
WEBTEST_DIR.mkdir(parents=True, exist_ok=True)
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)

results = {
    "test_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
    "module": "备课 (lesson-preparation)",
    "version": VERSION,
    "base_url": BASE_URL,
    "summary": {
        "total": 0,
        "passed": 0,
        "failed": 0,
        "warnings": 0,
    },
    "roles": {},
    "console_errors_global": [],
}


def _is_login_redirect(url: str) -> bool:
    """精确判断 URL 是否为登录页重定向"""
    parsed = urlparse(url)
    path = parsed.path.rstrip("/")
    if path.endswith("/login"):
        return True
    query = parse_qs(parsed.query)
    callback = query.get("callbackUrl", [""])[0]
    if callback and "/login" in callback:
        return True
    return False


def _is_forbidden_redirect(url: str) -> bool:
    """判断是否因权限不足被重定向（带 from/reason=forbidden 参数）"""
    parsed = urlparse(url)
    query = parse_qs(parsed.query)
    return query.get("reason", [""])[0] == "forbidden"


def safe_text(locator, max_len=500):
    try:
        text = locator.text_content()
        return (text or "").strip()[:max_len]
    except Exception:
        return ""


def login(page, role: str) -> bool:
    """登录指定角色账号"""
    account = TEST_ACCOUNTS[role]
    print(f"\n>>> 登录 {role} 账号 ({account['email']})...")

    page.goto(f"{BASE_URL}/login", timeout=30000)
    page.wait_for_load_state("networkidle", timeout=15000)

    current_path = urlparse(page.url).path
    if current_path.startswith(account["expected_path"]) or current_path.endswith("/dashboard"):
        print(f"    已登录，跳过登录步骤 (URL: {page.url})")
        return True

    try:
        email_input = page.locator('input[name="email"]')
        if email_input.count() == 0:
            email_input = page.locator('input[type="email"]')
        email_input.fill(account["email"])

        password_input = page.locator('input[name="password"]')
        if password_input.count() == 0:
            password_input = page.locator('input[type="password"]')
        password_input.fill(account["password"])

        login_btn = page.locator('button[type="submit"]')
        if login_btn.count() == 0:
            login_btn = page.get_by_role("button", name="Sign In with Email")
        if login_btn.count() == 0:
            login_btn = page.locator("button").filter(has_text="Sign In")
        if login_btn.count() == 0:
            login_btn = page.locator("button").filter(has_text="登录")
        login_btn.click()

        # 等待 URL 离开 /login（客户端导航，不能用 networkidle）
        try:
            page.wait_for_url(lambda url: "/login" not in url, timeout=20000)
        except PlaywrightTimeout:
            pass

        page.wait_for_timeout(3000)

        print(f"    登录后 URL: {page.url}")
        if "/login" in page.url:
            print(f"    ❌ {role} 登录失败")
            return False
        # parent 账号可能被重定向到 onboarding
        if "/onboarding" in page.url:
            print(f"    ⚠️ {role} 需要完成 onboarding，跳过该账号测试")
            return False
        return True
    except Exception as e:
        print(f"    ❌ {role} 登录异常: {e}")
        return False


def logout(page):
    """退出当前登录状态"""
    page.context.clear_cookies()
    print("    已清除 cookies 退出登录")


def collect_console_errors(page):
    """附加控制台错误收集器"""
    errors = []

    def on_console(msg):
        if msg.type == "error":
            text = msg.text
            if "favicon" in text.lower() or "Download the React DevTools" in text:
                return
            errors.append(text)
            results["console_errors_global"].append({"role": "current", "error": text})

    page.on("console", on_console)
    return errors, on_console


# ============ 角色访问权限测试 ============

def test_role_access(page, role: str) -> dict:
    """测试单个角色对备课模块的访问权限"""
    role_result = {
        "role": role,
        "login_success": False,
        "list_page": {"status": "unknown", "http_status": None, "final_url": None, "errors": [], "warnings": []},
        "new_page": {"status": "unknown", "http_status": None, "final_url": None, "errors": [], "warnings": []},
        "checks": [],
        "screenshots": [],
    }

    print(f"\n=== 测试 {role} 访问备课模块 ===")

    if not login(page, role):
        role_result["errors"] = [f"{role} 登录失败"]
        return role_result
    role_result["login_success"] = True

    # 访问列表页
    url = f"{BASE_URL}/teacher/lesson-plans"
    console_errors, on_console = collect_console_errors(page)
    try:
        response = page.goto(url, timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1500)

        http_status = response.status if response else None
        final_url = page.url
        role_result["list_page"]["http_status"] = http_status
        role_result["list_page"]["final_url"] = final_url

        # 截图
        screenshot_name = f"access_{role}_list.png"
        page.screenshot(path=str(SCREENSHOT_DIR / screenshot_name), full_page=True)
        role_result["screenshots"].append(screenshot_name)

        # 判定状态
        if _is_login_redirect(final_url):
            role_result["list_page"]["status"] = "failed"
            role_result["list_page"]["errors"].append("重定向到登录页")
        elif _is_forbidden_redirect(final_url):
            # 对于 student/parent 这是预期行为
            if role in ("student", "parent"):
                role_result["list_page"]["status"] = "passed"
                role_result["checks"].append({
                    "name": "无权限访问被正确拦截",
                    "passed": True,
                    "detail": f"重定向到 {final_url}"
                })
            else:
                role_result["list_page"]["status"] = "failed"
                role_result["list_page"]["errors"].append(f"权限不足被重定向到 {final_url}")
        elif http_status and http_status >= 500:
            role_result["list_page"]["status"] = "failed"
            role_result["list_page"]["errors"].append(f"HTTP {http_status}")
        elif http_status and http_status >= 400:
            role_result["list_page"]["status"] = "failed"
            role_result["list_page"]["errors"].append(f"HTTP {http_status}")
        else:
            # 成功访问
            if role in ("admin", "teacher"):
                role_result["list_page"]["status"] = "passed"
                role_result["checks"].append({
                    "name": "有权访问备课列表页",
                    "passed": True,
                    "detail": f"HTTP {http_status}"
                })
            else:
                role_result["list_page"]["status"] = "failed"
                role_result["list_page"]["errors"].append("无权限用户却成功访问了备课列表页")

        if console_errors:
            role_result["list_page"]["warnings"].append(f"控制台错误 {len(console_errors)} 条")
            if role_result["list_page"]["status"] == "passed":
                role_result["list_page"]["status"] = "warning"

    except PlaywrightTimeout:
        role_result["list_page"]["status"] = "failed"
        role_result["list_page"]["errors"].append("页面加载超时")
    except Exception as e:
        role_result["list_page"]["status"] = "failed"
        role_result["list_page"]["errors"].append(str(e)[:200])
    finally:
        try:
            page.remove_listener("console", on_console)
        except Exception:
            pass

    # 访问新建页（仅对 admin/teacher 测试）
    if role in ("admin", "teacher") and role_result["list_page"]["status"] in ("passed", "warning"):
        url = f"{BASE_URL}/teacher/lesson-plans/new"
        console_errors, on_console = collect_console_errors(page)
        try:
            response = page.goto(url, timeout=30000)
            page.wait_for_load_state("networkidle", timeout=15000)
            page.wait_for_timeout(1000)

            http_status = response.status if response else None
            final_url = page.url
            role_result["new_page"]["http_status"] = http_status
            role_result["new_page"]["final_url"] = final_url

            screenshot_name = f"access_{role}_new.png"
            page.screenshot(path=str(SCREENSHOT_DIR / screenshot_name), full_page=True)
            role_result["screenshots"].append(screenshot_name)

            if _is_login_redirect(final_url) or _is_forbidden_redirect(final_url):
                role_result["new_page"]["status"] = "failed"
                role_result["new_page"]["errors"].append(f"重定向到 {final_url}")
            elif http_status and http_status >= 400:
                role_result["new_page"]["status"] = "failed"
                role_result["new_page"]["errors"].append(f"HTTP {http_status}")
            else:
                role_result["new_page"]["status"] = "passed"

            if console_errors:
                role_result["new_page"]["warnings"].append(f"控制台错误 {len(console_errors)} 条")

        except Exception as e:
            role_result["new_page"]["status"] = "failed"
            role_result["new_page"]["errors"].append(str(e)[:200])
        finally:
            try:
                page.remove_listener("console", on_console)
            except Exception:
                pass

    return role_result


# ============ 教师完整功能测试 ============

def test_teacher_full_features(page: dict) -> dict:
    """测试教师对备课模块的完整功能：列表、新建、编辑、版本、复制、归档"""
    feature_result = {
        "role": "teacher",
        "features": {},
        "screenshots": [],
        "errors": [],
        "warnings": [],
    }

    print("\n=== 测试教师备课模块完整功能 ===")

    # ---- 1. 列表页功能 ----
    print("\n>>> [1/6] 测试列表页功能...")
    list_feature = {"name": "列表页", "status": "unknown", "checks": [], "errors": []}
    try:
        page.goto(f"{BASE_URL}/teacher/lesson-plans", timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1500)

        # 检查标题
        body_text = page.locator("body").text_content() or ""
        title_keys = ["备课", "Lesson Plan", "课案"]
        title_match = any(k in body_text for k in title_keys)
        list_feature["checks"].append({
            "name": "页面标题包含'备课'/'Lesson Plan'",
            "passed": title_match,
            "detail": f"body 长度 {len(body_text)}"
        })

        # 检查"新建"按钮
        new_btn = page.locator('a[href="/teacher/lesson-plans/new"]')
        new_btn_count = new_btn.count()
        list_feature["checks"].append({
            "name": "存在'新建课案'按钮",
            "passed": new_btn_count > 0,
            "detail": f"找到 {new_btn_count} 个"
        })

        # 检查筛选器
        filter_inputs = page.locator('input, select').count()
        list_feature["checks"].append({
            "name": "存在筛选器",
            "passed": filter_inputs > 0,
            "detail": f"找到 {filter_inputs} 个 input/select"
        })

        # 检查课案卡片（如果已有数据）
        cards = page.locator('a[href*="/teacher/lesson-plans/"]').count()
        list_feature["checks"].append({
            "name": "课案卡片或空状态",
            "passed": True,
            "detail": f"找到 {cards} 个课案链接"
        })

        page.screenshot(path=str(SCREENSHOT_DIR / "teacher_feature_list.png"), full_page=True)
        feature_result["screenshots"].append("teacher_feature_list.png")

        list_feature["status"] = "passed"
        print("    ✅ 列表页测试通过")
    except Exception as e:
        list_feature["status"] = "failed"
        list_feature["errors"].append(str(e)[:200])
        print(f"    ❌ 列表页测试失败: {e}")

    feature_result["features"]["list"] = list_feature

    # ---- 2. 新建课案 ----
    print("\n>>> [2/6] 测试新建课案...")
    create_feature = {"name": "新建课案", "status": "unknown", "checks": [], "errors": []}
    created_plan_id = None
    try:
        page.goto(f"{BASE_URL}/teacher/lesson-plans/new", timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(2000)

        # 检查模板选择器
        template_buttons = page.locator('button[type="button"]')
        template_count = template_buttons.count()
        create_feature["checks"].append({
            "name": "存在模板选择按钮",
            "passed": template_count > 0,
            "detail": f"找到 {template_count} 个按钮"
        })

        # 输入标题 - 使用更精确的选择器（required 属性的 input）
        title_input = page.locator('input[required]')
        if title_input.count() == 0:
            title_input = page.locator('input').first
        title_input.fill("【测试】自动化测试课案")
        page.wait_for_timeout(300)

        # 选择第一个模板（tpl_regular）- 使用更精确的选择器
        # 模板按钮有 text-left 类名和 border-2
        template_btn = page.locator('button[type="button"][class*="text-left"]').first
        if template_btn.count() == 0:
            # 回退：选择所有 type=button 且不是其他功能的按钮
            template_btn = template_buttons.first
        template_btn.click()
        page.wait_for_timeout(500)

        # 验证模板已选中（按钮应有 border-primary 类）
        selected_btn = page.locator('button[type="button"][class*="border-primary"]')
        create_feature["checks"].append({
            "name": "模板选中状态可视化",
            "passed": selected_btn.count() > 0,
            "detail": f"选中 {selected_btn.count()} 个"
        })

        # 点击创建按钮
        submit_btn = page.locator('button[type="submit"]')
        if submit_btn.count() == 0:
            submit_btn = page.locator('button').filter(has_text=re.compile(r"创建|Create|新建"))

        # 等待按钮可用
        try:
            page.wait_for_selector('button[type="submit"]:not([disabled])', timeout=5000)
        except PlaywrightTimeout:
            pass

        submit_btn.click()

        # 等待跳转到编辑页
        page.wait_for_timeout(2000)
        try:
            page.wait_for_url(re.compile(r'/teacher/lesson-plans/[^/]+/edit'), timeout=20000)
        except PlaywrightTimeout:
            page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(2000)

        final_url = page.url
        # 提取 planId
        match = re.search(r'/teacher/lesson-plans/([^/]+)/edit', final_url)
        if match:
            created_plan_id = match.group(1)
            create_feature["checks"].append({
                "name": "创建后跳转到编辑页",
                "passed": True,
                "detail": f"planId={created_plan_id}"
            })
        else:
            create_feature["checks"].append({
                "name": "创建后跳转到编辑页",
                "passed": False,
                "detail": f"URL={final_url}"
            })

        page.screenshot(path=str(SCREENSHOT_DIR / "teacher_feature_create.png"), full_page=True)
        feature_result["screenshots"].append("teacher_feature_create.png")

        create_feature["status"] = "passed" if created_plan_id else "failed"
        print(f"    ✅ 新建课案成功 (planId={created_plan_id})" if created_plan_id else "    ❌ 新建课案失败")
    except Exception as e:
        create_feature["status"] = "failed"
        create_feature["errors"].append(str(e)[:200])
        print(f"    ❌ 新建课案失败: {e}")

    feature_result["features"]["create"] = create_feature

    # ---- 3. 编辑器功能（节点图） ----
    print("\n>>> [3/6] 测试编辑器功能...")
    editor_feature = {"name": "编辑器", "status": "unknown", "checks": [], "errors": []}
    if created_plan_id:
        try:
            # 已经在编辑页
            page.wait_for_timeout(1500)

            # 检查标题输入框
            title_input = page.locator('input').first
            title_value = title_input.input_value() if title_input.count() > 0 else ""
            editor_feature["checks"].append({
                "name": "标题输入框存在且有值",
                "passed": bool(title_value),
                "detail": f"标题='{title_value}'"
            })

            # 检查 React Flow 节点画布
            rf_nodes = page.locator('.react-flow__node').count()
            editor_feature["checks"].append({
                "name": "React Flow 节点渲染",
                "passed": rf_nodes > 0,
                "detail": f"渲染 {rf_nodes} 个节点"
            })

            # 检查 React Flow 边
            rf_edges = page.locator('.react-flow__edge').count()
            editor_feature["checks"].append({
                "name": "React Flow 边渲染",
                "passed": rf_edges > 0 or rf_nodes <= 1,
                "detail": f"渲染 {rf_edges} 条边"
            })

            # 检查工具栏按钮
            save_btn = page.locator('button').filter(has_text=re.compile(r"保存|Save"))
            version_btn = page.locator('button').filter(has_text=re.compile(r"版本|History|Version"))
            add_node_btn = page.locator('button').filter(has_text=re.compile(r"添加|Add|节点|Node"))

            editor_feature["checks"].append({
                "name": "存在保存版本按钮",
                "passed": save_btn.count() > 0,
                "detail": f"找到 {save_btn.count()} 个"
            })
            editor_feature["checks"].append({
                "name": "存在版本历史按钮",
                "passed": version_btn.count() > 0,
                "detail": f"找到 {version_btn.count()} 个"
            })
            editor_feature["checks"].append({
                "name": "存在添加节点按钮",
                "passed": add_node_btn.count() > 0,
                "detail": f"找到 {add_node_btn.count()} 个"
            })

            page.screenshot(path=str(SCREENSHOT_DIR / "teacher_feature_editor.png"), full_page=True)
            feature_result["screenshots"].append("teacher_feature_editor.png")

            editor_feature["status"] = "passed"
            print(f"    ✅ 编辑器测试通过 ({rf_nodes} 节点, {rf_edges} 边)")
        except Exception as e:
            editor_feature["status"] = "failed"
            editor_feature["errors"].append(str(e)[:200])
            print(f"    ❌ 编辑器测试失败: {e}")
    else:
        editor_feature["status"] = "skipped"
        editor_feature["errors"].append("无创建的课案，跳过编辑器测试")
        print("    ⏭️  跳过编辑器测试（无创建的课案）")

    feature_result["features"]["editor"] = editor_feature

    # ---- 4. 节点选中与侧边面板 ----
    print("\n>>> [4/6] 测试节点选中与侧边面板...")
    panel_feature = {"name": "节点选中与侧边面板", "status": "unknown", "checks": [], "errors": []}
    if created_plan_id and editor_feature["status"] == "passed":
        try:
            # 点击第一个节点
            first_node = page.locator('.react-flow__node').first
            if first_node.count() > 0:
                first_node.click()
                page.wait_for_timeout(1000)

                # 检查侧边面板是否出现
                panel = page.locator('aside, [class*="panel"], [class*="sidebar"]').count()
                # 也检查具体的 NodeEditPanel（通常宽度 420px）
                panel_text = page.locator('body').text_content() or ""

                # 检查是否有 Block 编辑相关元素
                has_block_editor = (
                    page.locator('textarea, [contenteditable="true"], input[type="text"]').count() > 0
                )

                panel_feature["checks"].append({
                    "name": "点击节点后出现侧边面板",
                    "passed": has_block_editor,
                    "detail": f"找到编辑元素: {has_block_editor}"
                })

                page.screenshot(path=str(SCREENSHOT_DIR / "teacher_feature_panel.png"), full_page=True)
                feature_result["screenshots"].append("teacher_feature_panel.png")

                panel_feature["status"] = "passed" if has_block_editor else "failed"
                print(f"    ✅ 侧边面板测试 {'通过' if has_block_editor else '失败'}")
            else:
                panel_feature["status"] = "skipped"
                panel_feature["errors"].append("无节点可点击")
                print("    ⏭️  跳过侧边面板测试（无节点）")
        except Exception as e:
            panel_feature["status"] = "failed"
            panel_feature["errors"].append(str(e)[:200])
            print(f"    ❌ 侧边面板测试失败: {e}")
    else:
        panel_feature["status"] = "skipped"
        panel_feature["errors"].append("前置条件不满足")
        print("    ⏭️  跳过侧边面板测试")

    feature_result["features"]["panel"] = panel_feature

    # ---- 5. 版本历史 ----
    print("\n>>> [5/6] 测试版本历史...")
    version_feature = {"name": "版本历史", "status": "unknown", "checks": [], "errors": []}
    if created_plan_id:
        try:
            # 点击版本历史按钮
            version_btn = page.locator('button').filter(has_text=re.compile(r"版本|History|Version"))
            if version_btn.count() > 0:
                version_btn.first.click()
                page.wait_for_timeout(1500)

                # 检查抽屉是否打开
                drawer = page.locator('[class*="drawer"], [class*="sheet"], [role="dialog"]').count()
                version_feature["checks"].append({
                    "name": "版本历史抽屉打开",
                    "passed": drawer > 0,
                    "detail": f"找到 {drawer} 个 dialog/drawer"
                })

                page.screenshot(path=str(SCREENSHOT_DIR / "teacher_feature_versions.png"), full_page=True)
                feature_result["screenshots"].append("teacher_feature_versions.png")

                # 关闭抽屉（按 Escape）
                page.keyboard.press("Escape")
                page.wait_for_timeout(800)

                version_feature["status"] = "passed"
                print("    ✅ 版本历史测试通过")
            else:
                version_feature["status"] = "failed"
                version_feature["errors"].append("未找到版本历史按钮")
                print("    ❌ 未找到版本历史按钮")
        except Exception as e:
            version_feature["status"] = "failed"
            version_feature["errors"].append(str(e)[:200])
            print(f"    ❌ 版本历史测试失败: {e}")
    else:
        version_feature["status"] = "skipped"
        print("    ⏭️  跳过版本历史测试")

    feature_result["features"]["version"] = version_feature

    # ---- 6. 复制与归档（在列表页测试）----
    print("\n>>> [6/6] 测试复制与归档...")
    duplicate_archive_feature = {"name": "复制与归档", "status": "unknown", "checks": [], "errors": []}
    try:
        # 回到列表页
        page.goto(f"{BASE_URL}/teacher/lesson-plans", timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1500)

        # 查找课案卡片
        cards = page.locator('a[href*="/teacher/lesson-plans/"]').count()
        duplicate_archive_feature["checks"].append({
            "name": "列表页存在课案卡片",
            "passed": cards > 0,
            "detail": f"找到 {cards} 个"
        })

        if cards > 0:
            # 检查复制按钮
            duplicate_btn = page.locator('button').filter(has_text=re.compile(r"复制|Duplicate"))
            duplicate_archive_feature["checks"].append({
                "name": "存在复制按钮",
                "passed": duplicate_btn.count() > 0,
                "detail": f"找到 {duplicate_btn.count()} 个"
            })

            # 检查归档按钮
            archive_btn = page.locator('button').filter(has_text=re.compile(r"归档|Archive"))
            duplicate_archive_feature["checks"].append({
                "name": "存在归档按钮",
                "passed": archive_btn.count() > 0,
                "detail": f"找到 {archive_btn.count()} 个"
            })

            # 测试复制功能
            if duplicate_btn.count() > 0:
                initial_cards = page.locator('a[href*="/teacher/lesson-plans/"]').count()
                duplicate_btn.first.click()
                page.wait_for_timeout(2000)
                page.wait_for_load_state("networkidle", timeout=10000)
                page.wait_for_timeout(1000)
                after_cards = page.locator('a[href*="/teacher/lesson-plans/"]').count()
                duplicate_archive_feature["checks"].append({
                    "name": "复制后课案数量增加",
                    "passed": after_cards > initial_cards,
                    "detail": f"复制前 {initial_cards} → 复制后 {after_cards}"
                })

            page.screenshot(path=str(SCREENSHOT_DIR / "teacher_feature_duplicate.png"), full_page=True)
            feature_result["screenshots"].append("teacher_feature_duplicate.png")

            duplicate_archive_feature["status"] = "passed"
            print("    ✅ 复制与归档测试通过")
        else:
            duplicate_archive_feature["status"] = "warning"
            duplicate_archive_feature["errors"].append("列表页无课案卡片")
            print("    ⚠️ 列表页无课案卡片")
    except Exception as e:
        duplicate_archive_feature["status"] = "failed"
        duplicate_archive_feature["errors"].append(str(e)[:200])
        print(f"    ❌ 复制与归档测试失败: {e}")

    feature_result["features"]["duplicate_archive"] = duplicate_archive_feature

    return feature_result


# ============ 主流程 ============

def update_summary(role_result: dict):
    """根据角色测试结果更新汇总"""
    results["summary"]["total"] += 1
    statuses = []
    if "list_page" in role_result:
        statuses.append(role_result["list_page"]["status"])
    if "new_page" in role_result:
        statuses.append(role_result["new_page"]["status"])
    if "features" in role_result:
        for f in role_result["features"].values():
            statuses.append(f.get("status", "unknown"))

    if any(s == "failed" for s in statuses):
        results["summary"]["failed"] += 1
    elif any(s == "warning" for s in statuses):
        results["summary"]["warnings"] += 1
    elif any(s == "passed" for s in statuses):
        results["summary"]["passed"] += 1


def generate_markdown_report() -> str:
    """生成 Markdown 测试报告"""
    md = []
    md.append(f"# 备课模块（lesson-preparation）Web 测试报告 {VERSION}")
    md.append("")
    md.append(f"> 测试日期：{results['test_date']}")
    md.append(f"> 模块：{results['module']}")
    md.append(f"> Base URL：{results['base_url']}")
    md.append(f"> 测试方式：Playwright 自动化测试")
    md.append("")
    md.append("---")
    md.append("")
    md.append("## 一、测试概览")
    md.append("")
    s = results["summary"]
    md.append(f"| 指标 | 数值 |")
    md.append(f"|------|------|")
    md.append(f"| 测试角色总数 | {s['total']} |")
    md.append(f"| 通过 | {s['passed']} |")
    md.append(f"| 失败 | {s['failed']} |")
    md.append(f"| 警告 | {s['warnings']} |")
    md.append("")
    md.append("### 测试覆盖范围")
    md.append("")
    md.append("- **角色覆盖**：admin / teacher / student / parent")
    md.append("- **路由覆盖**：`/teacher/lesson-plans`（列表）、`/teacher/lesson-plans/new`（新建）、`/teacher/lesson-plans/[planId]/edit`（编辑）")
    md.append("- **功能覆盖**：列表查看、模板选择、新建课案、节点图画布、侧边面板、版本历史、复制、归档")
    md.append("- **权限测试**：student/parent 应被拒绝访问 `/teacher/*` 路由")
    md.append("")
    md.append("---")
    md.append("")
    md.append("## 二、角色访问权限测试")
    md.append("")
    md.append("| 角色 | 登录 | 列表页 | 新建页 | 备注 |")
    md.append("|------|------|--------|--------|------|")
    for role, info in results["roles"].items():
        login_ok = "✅" if info.get("login_success") else "❌"
        list_status = info.get("list_page", {}).get("status", "unknown")
        new_status = info.get("new_page", {}).get("status", "unknown")
        list_icon = {"passed": "✅", "failed": "❌", "warning": "⚠️", "unknown": "❓", "skipped": "⏭️"}.get(list_status, "❓")
        new_icon = {"passed": "✅", "failed": "❌", "warning": "⚠️", "unknown": "❓", "skipped": "⏭️"}.get(new_status, "❓")
        note = ""
        if role in ("student", "parent"):
            note = "应被拒绝（重定向到自己的 dashboard）"
        elif role == "admin":
            note = "只读访问"
        elif role == "teacher":
            note = "完整功能"
        md.append(f"| {role} | {login_ok} | {list_icon} | {new_icon} | {note} |")
    md.append("")
    md.append("---")
    md.append("")
    md.append("## 三、教师完整功能测试详情")
    md.append("")
    teacher_features = results.get("teacher_features", {})
    if teacher_features.get("features"):
        md.append("| 功能 | 状态 | 检查项数 | 通过数 | 错误 |")
        md.append("|------|------|----------|--------|------|")
        for key, f in teacher_features["features"].items():
            status = f.get("status", "unknown")
            icon = {"passed": "✅", "failed": "❌", "warning": "⚠️", "unknown": "❓", "skipped": "⏭️"}.get(status, "❓")
            checks = f.get("checks", [])
            passed = sum(1 for c in checks if c.get("passed"))
            errors = "; ".join(f.get("errors", [])) if f.get("errors") else "-"
            md.append(f"| {f.get('name', key)} | {icon} | {len(checks)} | {passed} | {errors} |")
        md.append("")

        md.append("### 详细检查项")
        md.append("")
        for key, f in teacher_features["features"].items():
            md.append(f"#### {f.get('name', key)}")
            md.append("")
            md.append(f"- **状态**：{f.get('status', 'unknown')}")
            if f.get("checks"):
                md.append("- **检查项**：")
                for c in f["checks"]:
                    icon = "✅" if c.get("passed") else "❌"
                    md.append(f"  - {icon} {c['name']}（{c.get('detail', '')}）")
            if f.get("errors"):
                md.append("- **错误**：")
                for e in f["errors"]:
                    md.append(f"  - {e}")
            md.append("")
    else:
        md.append("教师功能测试未执行或失败。")
        md.append("")
    md.append("---")
    md.append("")
    md.append("## 四、控制台错误汇总")
    md.append("")
    if results["console_errors_global"]:
        md.append(f"共收集到 {len(results['console_errors_global'])} 条控制台错误：")
        md.append("")
        for err in results["console_errors_global"][:20]:
            md.append(f"- `{err.get('error', '')[:200]}`")
        if len(results["console_errors_global"]) > 20:
            md.append(f"- ... 还有 {len(results['console_errors_global']) - 20} 条")
    else:
        md.append("✅ 无控制台错误")
    md.append("")
    md.append("---")
    md.append("")
    md.append("## 五、测试截图")
    md.append("")
    md.append(f"截图保存在 `webtest/screenshots/{MODULE_NAME}/` 目录下：")
    md.append("")
    for role, info in results["roles"].items():
        for shot in info.get("screenshots", []):
            md.append(f"- `{shot}`")
    if teacher_features.get("screenshots"):
        for shot in teacher_features["screenshots"]:
            md.append(f"- `{shot}`")
    md.append("")
    md.append("---")
    md.append("")
    md.append("## 六、测试结论")
    md.append("")
    s = results["summary"]
    if s["failed"] == 0 and s["warnings"] == 0:
        md.append("✅ **所有测试通过**。备课模块在所有角色下功能正常。")
    elif s["failed"] == 0:
        md.append(f"⚠️ **测试通过但有 {s['warnings']} 个警告**。建议检查警告项。")
    else:
        md.append(f"❌ **{s['failed']} 个测试失败**。需要修复以下问题：")
        md.append("")
        for role, info in results["roles"].items():
            list_err = info.get("list_page", {}).get("errors", [])
            new_err = info.get("new_page", {}).get("errors", [])
            if list_err or new_err:
                md.append(f"### {role}")
                for e in list_err:
                    md.append(f"- 列表页：{e}")
                for e in new_err:
                    md.append(f"- 新建页：{e}")
                md.append("")
        if teacher_features.get("features"):
            for key, f in teacher_features["features"].items():
                if f.get("status") == "failed" and f.get("errors"):
                    md.append(f"### 教师功能 - {f.get('name', key)}")
                    for e in f["errors"]:
                        md.append(f"- {e}")
                    md.append("")
    md.append("")
    md.append("---")
    md.append("")
    md.append("## 七、附录：测试账号")
    md.append("")
    md.append("| 角色 | 邮箱 | 预期路径 |")
    md.append("|------|------|----------|")
    for role, acc in TEST_ACCOUNTS.items():
        md.append(f"| {role} | {acc['email']} | {acc['expected_path']} |")
    md.append("")

    return "\n".join(md)


def main():
    print("=" * 60)
    print(f"备课模块（lesson-preparation）Web 测试 - {VERSION}")
    print("=" * 60)

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)

        # ---- 测试 4 个角色的访问权限 ----
        for role in ["admin", "teacher", "student", "parent"]:
            context = browser.new_context()
            page = context.new_page()
            try:
                role_result = test_role_access(page, role)
                results["roles"][role] = role_result
                update_summary(role_result)
            except Exception as e:
                print(f"❌ {role} 测试异常: {e}")
                results["roles"][role] = {
                    "role": role,
                    "login_success": False,
                    "errors": [str(e)[:200]],
                }
                results["summary"]["total"] += 1
                results["summary"]["failed"] += 1
            finally:
                context.close()

        # ---- 教师完整功能测试 ----
        context = browser.new_context()
        page = context.new_page()
        try:
            if not login(page, "teacher"):
                print("❌ 教师登录失败，跳过完整功能测试")
                results["teacher_features"] = {"features": {}, "errors": ["教师登录失败"]}
            else:
                teacher_features = test_teacher_full_features(page)
                results["teacher_features"] = teacher_features
                # 教师功能测试结果也计入汇总
                feature_failed = sum(1 for f in teacher_features.get("features", {}).values() if f.get("status") == "failed")
                if feature_failed > 0:
                    results["summary"]["failed"] += feature_failed
        except Exception as e:
            print(f"❌ 教师功能测试异常: {e}")
            results["teacher_features"] = {"features": {}, "errors": [str(e)[:200]]}
            results["summary"]["failed"] += 1
        finally:
            context.close()

        browser.close()

    # 生成报告
    print("\n" + "=" * 60)
    print("生成测试报告...")
    print("=" * 60)

    md_report = generate_markdown_report()
    md_path = WEBTEST_DIR / f"{MODULE_NAME}_{VERSION}.md"
    md_path.write_text(md_report, encoding="utf-8")
    print(f"✅ Markdown 报告: {md_path}")

    json_path = WEBTEST_DIR / f"{MODULE_NAME}_{VERSION}.json"
    json_path.write_text(json.dumps(results, ensure_ascii=False, indent=2), encoding="utf-8")
    print(f"✅ JSON 报告: {json_path}")

    # 打印汇总
    print("\n" + "=" * 60)
    print("测试汇总")
    print("=" * 60)
    s = results["summary"]
    print(f"  总计: {s['total']}")
    print(f"  通过: {s['passed']}")
    print(f"  失败: {s['failed']}")
    print(f"  警告: {s['warnings']}")
    print(f"\n详细报告: {md_path}")

    # 失败时返回非零退出码
    if s["failed"] > 0:
        sys.exit(1)


if __name__ == "__main__":
    main()
