"""
家长端全功能Web测试脚本
使用 Playwright 对家长端所有页面进行功能测试
"""
import json
import os
import re
import sys
from datetime import datetime
from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeout

BASE_URL = "http://localhost:3000"
PARENT_EMAIL = "parent_g1c1_1@xiaoxue.edu.cn"
PARENT_PASSWORD = "123456"

# 家长端所有路由（从导航配置 navigation.ts 和源码中提取）
# 家长权限：EXAM_READ, TEXTBOOK_READ, CLASS_READ, USER_PROFILE_UPDATE,
#           ANNOUNCEMENT_READ, GRADE_RECORD_READ, ATTENDANCE_READ,
#           MESSAGE_SEND, MESSAGE_READ, MESSAGE_DELETE
PARENT_ROUTES = {
    "Dashboard": ["/parent/dashboard"],
    "Grades": ["/parent/grades"],
    "Attendance": ["/parent/attendance"],
    "Announcements": ["/announcements"],
    "Messages": [
        "/messages",
        "/messages/compose",
    ],
    "Profile": ["/profile"],
    "Settings": [
        "/settings",
        "/settings/security",
    ],
}

# 详情页 - 需要从 dashboard 获取 studentId
DETAIL_ROUTES = {
    "Child Detail": "/parent/children/",
}

# 跨角色访问保护测试：家长不应能访问教师/学生/管理员页面
FORBIDDEN_ROUTES = [
    "/admin/dashboard",
    "/admin/school",
    "/teacher/dashboard",
    "/teacher/exams",
    "/teacher/homework",
    "/teacher/grades",
    "/teacher/questions",
    "/teacher/classes",
    "/teacher/attendance",
    "/student/dashboard",
    "/student/learning",
    "/student/grades",
    "/student/attendance",
    "/management/grade/classes",
]

results = {
    "test_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
    "test_target": "家长端 (Parent)",
    "base_url": BASE_URL,
    "parent_email": PARENT_EMAIL,
    "summary": {
        "total": 0,
        "passed": 0,
        "failed": 0,
        "warnings": 0,
    },
    "pages": {},
    "functional_checks": [],
    "security_checks": [],
    "console_errors": [],
    "navigation_issues": [],
}


def login(page):
    """登录家长账号"""
    print("\n>>> 登录家长账号...")
    page.goto(f"{BASE_URL}/login")
    page.wait_for_load_state("networkidle")

    # 检查是否已登录
    if "/parent" in page.url or (page.url.rstrip("/").endswith("/dashboard") and "/login" not in page.url):
        print("    已登录，跳过登录步骤")
        return True

    try:
        email_input = page.locator('input[name="email"]')
        if email_input.count() == 0:
            email_input = page.locator('input[type="email"]')
        print(f"    找到 email 输入框: {email_input.count()} 个")
        email_input.fill(PARENT_EMAIL)

        password_input = page.locator('input[name="password"]')
        if password_input.count() == 0:
            password_input = page.locator('input[type="password"]')
        print(f"    找到 password 输入框: {password_input.count()} 个")
        password_input.fill(PARENT_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="登录")
        print(f"    找到登录按钮: {login_btn.count()} 个")
        login_btn.click()

        # 等待导航或错误提示
        try:
            page.wait_for_url(lambda url: "/login" not in url, timeout=10000)
        except PlaywrightTimeout:
            # 登录可能失败，检查页面上的错误
            body_text = page.locator("body").text_content() or ""
            print(f"    登录后仍在 login 页面，页面内容片段: {body_text[:300]}")

        page.wait_for_load_state("networkidle")
        page.wait_for_timeout(2000)

        print(f"    登录后 URL: {page.url}")
        return "/login" not in page.url
    except Exception as e:
        print(f"    ❌ 登录失败: {e}")
        return False


def test_page(page, route, category, expect_redirect_to_login=False, expect_forbidden_redirect=False):
    """测试单个页面"""
    url = f"{BASE_URL}{route}"
    page_result = {
        "url": url,
        "route": route,
        "category": category,
        "status": "unknown",
        "http_status": None,
        "final_url": None,
        "redirect_url": None,
        "errors": [],
        "warnings": [],
        "content_checks": [],
    }

    print(f"\n  测试: {category} - {route}")

    # 收集控制台错误
    console_errors = []
    MAX_CONSOLE_ERRORS = 5

    def on_console(msg):
        if msg.type == "error":
            # 限制收集的错误数量，避免内存爆炸
            if len(console_errors) >= MAX_CONSOLE_ERRORS:
                return
            # 截断过长的控制台错误（避免堆栈跟踪导致输出爆炸）
            text = msg.text
            if len(text) > 500:
                text = text[:500] + "...（已截断）"
            console_errors.append(text)

    page.on("console", on_console)

    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

        page_result["http_status"] = http_status
        page_result["final_url"] = final_url

        # 检查是否重定向
        if final_url.rstrip("/") != url.rstrip("/"):
            page_result["redirect_url"] = final_url

        # 检查 HTTP 状态
        if http_status and http_status >= 500:
            if expect_forbidden_redirect:
                # 跨角色访问导致 500 也是权限隔离失效的表现
                page_result["status"] = "failed"
                page_result["errors"].append(f"跨角色访问返回 HTTP {http_status}（应被重定向拦截）")
            else:
                page_result["status"] = "failed"
                page_result["errors"].append(f"HTTP {http_status} error")
        elif http_status and http_status >= 400:
            page_result["status"] = "warning"
            page_result["warnings"].append(f"HTTP {http_status} error")
        elif final_url and "/login" in final_url:
            if expect_redirect_to_login or expect_forbidden_redirect:
                page_result["status"] = "passed"
                page_result["content_checks"].append("正确重定向到登录页（权限拦截）")
            else:
                page_result["status"] = "failed"
                page_result["errors"].append("Redirected to login - auth issue")
        elif final_url and "/500" in final_url:
            page_result["status"] = "failed"
            page_result["errors"].append("Redirected to 500 error page")
        elif final_url and "/404" in final_url:
            page_result["status"] = "warning"
            page_result["warnings"].append("Redirected to 404 page")
        elif expect_forbidden_redirect:
            # 期望被拦截但实际访问成功的跨角色访问
            if "/parent/" in final_url and "reason=forbidden" in final_url:
                page_result["status"] = "passed"
                page_result["content_checks"].append("跨角色访问被权限系统拦截")
            elif "/parent/" in final_url:
                page_result["status"] = "passed"
                page_result["content_checks"].append(f"跨角色访问被拦截，重定向到: {final_url}")
            else:
                # 仍然停留在教师/学生/管理员页面 = 权限隔离失效
                page_result["status"] = "failed"
                page_result["errors"].append(
                    f"⚠️ 安全漏洞：家长成功访问了受限页面（最终 URL: {final_url}），权限隔离失效"
                )
        else:
            page_result["status"] = "passed"

        # 检查页面是否有错误提示
        error_texts = page.locator('[role="alert"], .error, .text-destructive, .text-red-500').all()
        for et in error_texts:
            try:
                text = et.text_content()
                if text and text.strip() and "Access denied" not in text:
                    page_result["warnings"].append(f"Error text on page: {text.strip()[:100]}")
            except Exception:
                pass

        # 检查页面是否为空（无主要内容）
        body_text = page.locator("body").text_content() or ""
        if len(body_text.strip()) < 50:
            page_result["warnings"].append("Page appears empty or has very little content")

        # 收集控制台错误
        if console_errors:
            page_result["errors"].extend(console_errors[:5])

        # 打印状态
        status_icon = "✅" if page_result["status"] == "passed" else "⚠️" if page_result["status"] == "warning" else "❌"
        print(f"    {status_icon} {page_result['status']} (HTTP {http_status})")
        if page_result.get("redirect_url"):
            print(f"    ↪ 重定向到: {page_result['redirect_url']}")

    except PlaywrightTimeout:
        page_result["status"] = "failed"
        page_result["errors"].append("Page load timeout (30s)")
        print(f"    ❌ TIMEOUT")
    except Exception as e:
        page_result["status"] = "failed"
        page_result["errors"].append(str(e)[:200])
        print(f"    ❌ ERROR: {str(e)[:100]}")

    finally:
        page.remove_listener("console", on_console)

    return page_result


def discover_child_links(page):
    """从家长仪表盘发现子女详情页链接"""
    try:
        url = f"{BASE_URL}/parent/dashboard"
        page.goto(url, timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1500)

        # 查找子女详情链接
        links = page.locator('a[href*="/parent/children/"]').all()
        detail_urls = []
        seen = set()
        for link in links:
            href = link.get_attribute("href")
            if href and "/parent/children/" in href:
                # 提取纯路径（不含 query string）
                clean_href = href.split("?")[0]
                if clean_href not in seen:
                    seen.add(clean_href)
                    detail_urls.append(clean_href)
        return detail_urls
    except Exception as e:
        print(f"    发现子女链接失败: {e}")
        return []


def check_dashboard_functionality(page):
    """检查家长仪表盘功能完整性"""
    checks = []

    print("\n>>> 检查仪表盘功能完整性...")

    try:
        page.goto(f"{BASE_URL}/parent/dashboard", timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1500)

        # 1. 检查标题
        title = page.locator("h1")
        title_text = title.first.text_content() if title.count() > 0 else ""
        checks.append({
            "name": "仪表盘标题",
            "expected": "Parent Dashboard",
            "actual": title_text.strip() if title_text else "",
            "passed": "Parent Dashboard" in (title_text or ""),
        })

        # 2. 检查问候语
        body_text = page.locator("body").text_content() or ""
        greeting_present = any(g in body_text for g in ["Good morning", "Good afternoon", "Good evening", "Welcome"])
        checks.append({
            "name": "问候语显示",
            "expected": "Good morning/afternoon/evening 或 Welcome",
            "actual": "Found" if greeting_present else "Missing",
            "passed": greeting_present,
        })

        # 3. 检查快捷入口按钮
        grades_btn = page.locator('a[href="/parent/grades"]')
        attendance_btn = page.locator('a[href="/parent/attendance"]')
        announcements_btn = page.locator('a[href="/announcements"]')

        checks.append({
            "name": "Grades 快捷入口",
            "expected": "存在",
            "actual": "Found" if grades_btn.count() > 0 else "Missing",
            "passed": grades_btn.count() > 0,
        })
        checks.append({
            "name": "Attendance 快捷入口",
            "expected": "存在",
            "actual": "Found" if attendance_btn.count() > 0 else "Missing",
            "passed": attendance_btn.count() > 0,
        })
        checks.append({
            "name": "Announcements 快捷入口",
            "expected": "存在",
            "actual": "Found" if announcements_btn.count() > 0 else "Missing",
            "passed": announcements_btn.count() > 0,
        })

        # 4. 检查子女卡片
        child_cards = page.locator('a[href*="/parent/children/"]')
        card_count = child_cards.count()
        checks.append({
            "name": "子女卡片显示",
            "expected": "≥1 个子女卡片",
            "actual": f"{card_count} 个",
            "passed": card_count > 0,
        })

        # 5. 检查子女卡片内容（Pending/Overdue/Avg 统计）
        if card_count > 0:
            has_pending = "Pending" in body_text
            has_overdue = "Overdue" in body_text
            has_avg = "Avg" in body_text or "%" in body_text
            checks.append({
                "name": "子女卡片 - Pending 统计",
                "expected": "显示 Pending 计数",
                "actual": "Found" if has_pending else "Missing",
                "passed": has_pending,
            })
            checks.append({
                "name": "子女卡片 - Overdue 统计",
                "expected": "显示 Overdue 计数",
                "actual": "Found" if has_overdue else "Missing",
                "passed": has_overdue,
            })

        # 6. 检查子女数量提示
        children_count_text = "child linked" in body_text or "children linked" in body_text
        checks.append({
            "name": "子女数量提示",
            "expected": "显示 'N child(ren) linked'",
            "actual": "Found" if children_count_text else "Missing",
            "passed": children_count_text,
        })

    except Exception as e:
        checks.append({
            "name": "仪表盘功能检查",
            "expected": "无异常",
            "actual": str(e)[:200],
            "passed": False,
        })

    for c in checks:
        icon = "✅" if c["passed"] else "❌"
        print(f"    {icon} {c['name']}: {c['actual']}")

    return checks


def check_child_detail_functionality(page, child_route):
    """检查子女详情页功能完整性"""
    checks = []

    print(f"\n>>> 检查子女详情页功能: {child_route}")

    try:
        url = f"{BASE_URL}{child_route}"
        page.goto(url, timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1500)

        body_text = page.locator("body").text_content() or ""

        # 1. 检查返回按钮
        back_btn = page.locator('a[href="/parent/dashboard"]')
        checks.append({
            "name": "返回仪表盘按钮",
            "expected": "存在 Back to Dashboard 链接",
            "actual": "Found" if back_btn.count() > 0 else "Missing",
            "passed": back_btn.count() > 0,
        })

        # 2. 检查子女姓名标题
        h1 = page.locator("h1")
        h1_text = h1.first.text_content() if h1.count() > 0 else ""
        checks.append({
            "name": "子女姓名标题",
            "expected": "显示子女姓名",
            "actual": h1_text.strip()[:50] if h1_text else "Missing",
            "passed": bool(h1_text and h1_text.strip() and h1_text.strip() != "Unnamed"),
        })

        # 3. 检查邮箱掩码
        # 邮箱应该被掩码为 j***@example.com 形式
        email_pattern = re.search(r"[a-zA-Z0-9]\*\*\*@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", body_text)
        checks.append({
            "name": "邮箱掩码处理",
            "expected": "邮箱被掩码为 j***@domain.com",
            "actual": "Masked" if email_pattern else "Not masked or no email",
            "passed": bool(email_pattern),
        })

        # 4. 检查作业摘要卡片
        has_homework = "Homework" in body_text
        checks.append({
            "name": "作业摘要卡片",
            "expected": "显示 {childName}'s Homework",
            "actual": "Found" if has_homework else "Missing",
            "passed": has_homework,
        })

        # 5. 检查作业统计计数
        has_pending = "Pending" in body_text
        has_submitted = "Submitted" in body_text
        has_graded = "Graded" in body_text
        checks.append({
            "name": "作业统计 - Pending",
            "expected": "显示 Pending 计数",
            "actual": "Found" if has_pending else "Missing",
            "passed": has_pending,
        })
        checks.append({
            "name": "作业统计 - Submitted",
            "expected": "显示 Submitted 计数",
            "actual": "Found" if has_submitted else "Missing",
            "passed": has_submitted,
        })
        checks.append({
            "name": "作业统计 - Graded",
            "expected": "显示 Graded 计数",
            "actual": "Found" if has_graded else "Missing",
            "passed": has_graded,
        })

        # 6. 检查成绩趋势卡片
        has_grade_summary = "Grade" in body_text or "grade" in body_text.lower()
        checks.append({
            "name": "成绩趋势卡片",
            "expected": "显示成绩信息",
            "actual": "Found" if has_grade_summary else "Missing",
            "passed": has_grade_summary,
        })

        # 7. 检查今日课表卡片
        has_schedule = "Today Schedule" in body_text or "Schedule" in body_text
        checks.append({
            "name": "今日课表卡片",
            "expected": "显示 {childName}'s Today Schedule",
            "actual": "Found" if has_schedule else "Missing",
            "passed": has_schedule,
        })

        # 8. 检查 View all 链接
        view_all = page.locator("text=View all")
        checks.append({
            "name": "View all 链接",
            "expected": "存在 View all 链接",
            "actual": "Found" if view_all.count() > 0 else "Missing",
            "passed": view_all.count() > 0,
        })

    except Exception as e:
        checks.append({
            "name": "子女详情页功能检查",
            "expected": "无异常",
            "actual": str(e)[:200],
            "passed": False,
        })

    for c in checks:
        icon = "✅" if c["passed"] else "❌"
        print(f"    {icon} {c['name']}: {c['actual']}")

    return checks


def check_security_isolation(page):
    """检查跨家庭信息隔离（访问其他家长的子女应被拒绝）"""
    checks = []

    print("\n>>> 检查跨家庭信息隔离...")

    try:
        # 尝试访问一个不存在的 studentId
        url = f"{BASE_URL}/parent/children/non_existent_student_id_12345"
        page.goto(url, timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1500)

        body_text = page.locator("body").text_content() or ""
        # 应该显示 Access denied 或 404
        is_denied = "Access denied" in body_text or "not linked" in body_text
        is_404 = "404" in body_text or "not found" in body_text.lower() or "Page Not Found" in body_text

        checks.append({
            "name": "访问不存在/非关联子女应被拒绝",
            "expected": "显示 Access denied 或 404",
            "actual": "Access denied" if is_denied else ("404" if is_404 else "未拦截"),
            "passed": is_denied or is_404,
        })

    except Exception as e:
        checks.append({
            "name": "跨家庭隔离检查",
            "expected": "无异常",
            "actual": str(e)[:200],
            "passed": False,
        })

    for c in checks:
        icon = "✅" if c["passed"] else "❌"
        print(f"    {icon} {c['name']}: {c['actual']}")

    return checks


def check_sidebar_navigation(page):
    """检查侧边栏导航是否仅显示家长相关菜单"""
    checks = []

    print("\n>>> 检查侧边栏导航...")

    try:
        page.goto(f"{BASE_URL}/parent/dashboard", timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1500)

        # 仅检查侧边栏区域内的导航链接文本，避免页面正文误判
        # 侧边栏通常在 nav 元素或 [data-sidebar] 内
        sidebar = page.locator("nav").first
        if sidebar.count() == 0:
            sidebar = page.locator("body")
        sidebar_text = sidebar.text_content() or ""

        # 家长端应显示的导航项
        expected_navs = ["Dashboard", "Grades", "Attendance", "Announcements", "Messages"]
        for nav in expected_navs:
            checks.append({
                "name": f"侧边栏 - {nav}",
                "expected": f"显示 {nav} 导航项",
                "actual": "Found" if nav in sidebar_text else "Missing",
                "passed": nav in sidebar_text,
            })

        # 家长端不应显示的教师/管理员导航项
        forbidden_navs = ["Textbooks", "Question Bank", "Class Management", "Course Plans", "Lesson Plans", "Schedule Changes", "Diagnostic", "Electives", "School Management", "Audit Logs"]
        for nav in forbidden_navs:
            if nav in sidebar_text:
                checks.append({
                    "name": f"侧边栏 - {nav}（不应显示）",
                    "expected": f"不显示 {nav}",
                    "actual": "Found (违规)",
                    "passed": False,
                })

    except Exception as e:
        checks.append({
            "name": "侧边栏导航检查",
            "expected": "无异常",
            "actual": str(e)[:200],
            "passed": False,
        })

    for c in checks:
        icon = "✅" if c["passed"] else "❌"
        print(f"    {icon} {c['name']}: {c['actual']}")

    return checks


def run_all_tests():
    """运行所有测试"""
    global results

    with sync_playwright() as p:
        browser = p.chromium.launch(headless=True)
        context = browser.new_context(
            viewport={"width": 1440, "height": 900},
            locale="zh-CN",
            ignore_https_errors=True,
        )
        page = context.new_page()

        # 登录
        if not login(page):
            print("❌ 登录失败，终止测试")
            browser.close()
            return

        # 测试所有路由
        total = 0
        for category, routes in PARENT_ROUTES.items():
            for route in routes:
                page_result = test_page(page, route, category)
                key = route.replace("/", "_").strip("_")
                results["pages"][key] = page_result
                total += 1

        # 发现并测试子女详情页
        print("\n\n>>> 发现子女详情页链接...")
        child_links = discover_child_links(page)
        if child_links:
            print(f"\n  发现 {len(child_links)} 个子女详情页链接")
            for child_route in child_links[:3]:  # 最多测试3个
                if not child_route.startswith("/"):
                    child_route = "/" + child_route
                page_result = test_page(page, child_route, "Child Detail")
                key = child_route.replace("/", "_").strip("_")
                results["pages"][key] = page_result
                total += 1

                # 对子女详情页进行功能检查
                detail_checks = check_child_detail_functionality(page, child_route)
                results["functional_checks"].extend(detail_checks)
        else:
            print("\n  未发现子女详情页链接（可能家长未关联子女）")

        # 仪表盘功能完整性检查
        dashboard_checks = check_dashboard_functionality(page)
        results["functional_checks"].extend(dashboard_checks)

        # 侧边栏导航检查
        sidebar_checks = check_sidebar_navigation(page)
        results["functional_checks"].extend(sidebar_checks)

        # 跨家庭信息隔离检查
        security_checks = check_security_isolation(page)
        results["security_checks"].extend(security_checks)

        # 跨角色访问保护测试
        print("\n\n>>> 测试跨角色访问保护...")
        for forbidden_route in FORBIDDEN_ROUTES:
            page_result = test_page(
                page, forbidden_route, "Cross-Role Access Control",
                expect_forbidden_redirect=True,
            )
            key = "forbidden_" + forbidden_route.replace("/", "_").strip("_")
            results["pages"][key] = page_result
            total += 1

        # 汇总
        passed = sum(1 for p in results["pages"].values() if p["status"] == "passed")
        failed = sum(1 for p in results["pages"].values() if p["status"] == "failed")
        warnings = sum(1 for p in results["pages"].values() if p["status"] == "warning")

        results["summary"]["total"] = total
        results["summary"]["passed"] = passed
        results["summary"]["failed"] = failed
        results["summary"]["warnings"] = warnings

        print(f"\n\n{'='*60}")
        print(f"页面测试完成: 总计 {total}, 通过 {passed}, 失败 {failed}, 警告 {warnings}")
        print(f"{'='*60}")

        # 功能检查汇总
        func_passed = sum(1 for c in results["functional_checks"] if c["passed"])
        func_total = len(results["functional_checks"])
        print(f"功能检查: 总计 {func_total}, 通过 {func_passed}")
        print(f"安全检查: 总计 {len(results['security_checks'])}, 通过 {sum(1 for c in results['security_checks'] if c['passed'])}")
        print(f"{'='*60}")

        browser.close()


def generate_report():
    """生成Markdown测试报告"""
    report_lines = []
    report_lines.append("# 家长端 Web 功能测试报告")
    report_lines.append("")
    report_lines.append(f"> 测试日期：{results['test_date']}")
    report_lines.append(f"> 测试范围：家长端所有页面功能 + 跨角色权限隔离")
    report_lines.append(f"> 测试工具：Playwright + Chromium (headless)")
    report_lines.append(f"> 测试账号：{results['parent_email']}")
    report_lines.append(f"> Base URL：{results['base_url']}")
    report_lines.append("")
    report_lines.append("---")
    report_lines.append("")
    report_lines.append("## 一、测试概览")
    report_lines.append("")
    s = results["summary"]
    report_lines.append(f"| 指标 | 数值 |")
    report_lines.append(f"|------|------|")
    report_lines.append(f"| 总测试页面数 | {s['total']} |")
    report_lines.append(f"| 通过 | {s['passed']} |")
    report_lines.append(f"| 失败 | {s['failed']} |")
    report_lines.append(f"| 警告 | {s['warnings']} |")
    report_lines.append(f"| 页面通过率 | {s['passed']/s['total']*100:.1f}% |" if s['total'] > 0 else "| 通过率 | N/A |")

    func_total = len(results["functional_checks"])
    func_passed = sum(1 for c in results["functional_checks"] if c["passed"])
    sec_total = len(results["security_checks"])
    sec_passed = sum(1 for c in results["security_checks"] if c["passed"])
    report_lines.append(f"| 功能检查通过率 | {func_passed}/{func_total} ({func_passed/func_total*100:.1f}%) |" if func_total > 0 else "| 功能检查 | N/A |")
    report_lines.append(f"| 安全检查通过率 | {sec_passed}/{sec_total} ({sec_passed/sec_total*100:.1f}%) |" if sec_total > 0 else "| 安全检查 | N/A |")
    report_lines.append("")

    # 关键发现摘要
    report_lines.append("---")
    report_lines.append("")
    report_lines.append("## 二、关键发现")
    report_lines.append("")

    failed_pages = [(k, v) for k, v in results["pages"].items() if v["status"] == "failed"]
    if failed_pages:
        # 分类失败项
        security_failures = []
        for k, p in failed_pages:
            if p["category"] == "Cross-Role Access Control":
                security_failures.append(p)

        if security_failures:
            report_lines.append("### ⚠️ 严重：跨角色访问控制失效（安全漏洞）")
            report_lines.append("")
            report_lines.append("家长账号可以访问教师端页面，权限隔离失效。根因分析：")
            report_lines.append("")
            report_lines.append("- [`src/proxy.ts`](../src/proxy.ts#L10-L16) 中 `/teacher` 路由前缀仅要求 `EXAM_READ` 权限")
            report_lines.append("- [`src/shared/lib/permissions.ts`](../src/shared/lib/permissions.ts#L125-L136) 中家长角色被授予了 `EXAM_READ` 权限")
            report_lines.append("- 因此家长通过了 proxy 的权限检查，可以访问所有 `/teacher/*` 页面")
            report_lines.append("")
            report_lines.append("受影响页面：")
            report_lines.append("")
            report_lines.append("| 路由 | HTTP | 表现 |")
            report_lines.append("|------|------|------|")
            for p in security_failures:
                route = p.get("route", "")
                http = p["http_status"] or "-"
                if p["http_status"] and p["http_status"] >= 500:
                   表现 = f"HTTP 500（页面崩溃）"
                elif p.get("redirect_url"):
                    表现 = f"成功访问并重定向到 `{p['redirect_url'].replace(BASE_URL, '')}`"
                else:
                    表现 = "成功访问（HTTP 200）"
                report_lines.append(f"| `{route}` | {http} | {表现} |")
            report_lines.append("")
            report_lines.append("**修复建议**：")
            report_lines.append("")
            report_lines.append("1. 在 `src/proxy.ts` 中为 `/teacher` 路由前缀增加角色校验（要求 `teacher` / `grade_head` / `teaching_head` 角色），或")
            report_lines.append("2. 在 `src/shared/lib/permissions.ts` 中移除家长角色的 `EXAM_READ` 权限（如果家长不需要查看考试），或")
            report_lines.append("3. 在各教师端页面的 Server Component 中增加 `requireRole()` 角色校验，作为深度防御")
            report_lines.append("")

    passed_pages = [(k, v) for k, v in results["pages"].items() if v["status"] == "passed" and v["category"] != "Cross-Role Access Control"]
    if passed_pages:
        report_lines.append("### ✅ 家长端核心功能正常")
        report_lines.append("")
        report_lines.append(f"- 家长端 {len(passed_pages)} 个页面全部正常加载（HTTP 200）")
        report_lines.append(f"- 功能完整性检查 {func_passed}/{func_total} 项通过")
        report_lines.append(f"- 跨家庭信息隔离正常工作（访问非关联子女返回 Access denied）")
        report_lines.append(f"- 侧边栏导航正确显示家长菜单，未泄露教师/管理员菜单")
        report_lines.append(f"- 子女详情页邮箱掩码、作业摘要、成绩趋势、今日课表等功能完整")
        report_lines.append("")

    report_lines.append("---")
    report_lines.append("")
    report_lines.append("## 三、页面测试详情")
    report_lines.append("")

    # 按类别分组
    by_category = {}
    for key, page_result in results["pages"].items():
        cat = page_result.get("category", "Other")
        by_category.setdefault(cat, []).append(page_result)

    for category in sorted(by_category.keys()):
        pages = by_category[category]
        report_lines.append(f"### {category}")
        report_lines.append("")
        report_lines.append("| 状态 | 路由 | HTTP | 结果 | 备注 |")
        report_lines.append("|------|------|------|------|------|")

        for p in pages:
            status_icon = "✅" if p["status"] == "passed" else "⚠️" if p["status"] == "warning" else "❌"
            notes = []
            if p.get("redirect_url"):
                notes.append(f"重定向: `{p['redirect_url'].replace(BASE_URL, '')}`")
            if p.get("errors"):
                # 截断过长的错误信息
                for err in p["errors"][:2]:
                    # 只取第一行，避免堆栈跟踪污染
                    short_err = err.split("\n")[0][:150]
                    notes.append(f"错误: {short_err}")
            if p.get("warnings"):
                for w in p["warnings"][:2]:
                    short_w = w.split("\n")[0][:150]
                    notes.append(f"警告: {short_w}")
            if p.get("content_checks"):
                notes.extend(p["content_checks"][:2])
            note_str = "<br>".join(notes) if notes else "-"

            short_route = p.get("route", p["url"].replace(BASE_URL, ""))
            report_lines.append(
                f"| {status_icon} | `{short_route}` | {p['http_status'] or '-'} | {p['status']} | {note_str} |"
            )

        report_lines.append("")

    # 功能检查详情
    if results["functional_checks"]:
        report_lines.append("---")
        report_lines.append("")
        report_lines.append("## 四、功能完整性检查")
        report_lines.append("")
        report_lines.append("| 状态 | 检查项 | 期望 | 实际 |")
        report_lines.append("|------|--------|------|------|")
        for c in results["functional_checks"]:
            icon = "✅" if c["passed"] else "❌"
            report_lines.append(f"| {icon} | {c['name']} | {c['expected']} | {c['actual']} |")
        report_lines.append("")

    # 安全检查详情
    if results["security_checks"]:
        report_lines.append("---")
        report_lines.append("")
        report_lines.append("## 五、安全检查")
        report_lines.append("")
        report_lines.append("| 状态 | 检查项 | 期望 | 实际 |")
        report_lines.append("|------|--------|------|------|")
        for c in results["security_checks"]:
            icon = "✅" if c["passed"] else "❌"
            report_lines.append(f"| {icon} | {c['name']} | {c['expected']} | {c['actual']} |")
        report_lines.append("")

    # 失败页面汇总
    failed_pages = [(k, v) for k, v in results["pages"].items() if v["status"] == "failed"]
    if failed_pages:
        report_lines.append("---")
        report_lines.append("")
        report_lines.append("## 六、失败页面详情")
        report_lines.append("")
        for key, p in failed_pages:
            report_lines.append(f"### ❌ `{p.get('route', p['url'])}`")
            report_lines.append("")
            report_lines.append(f"- **分类**: {p['category']}")
            report_lines.append(f"- **HTTP状态**: {p['http_status']}")
            if p.get("redirect_url"):
                report_lines.append(f"- **重定向**: `{p['redirect_url']}`")
            if p.get("errors"):
                report_lines.append(f"- **错误信息**:")
                for err in p["errors"]:
                    # 截断过长的错误信息，只取前 300 字符
                    short_err = err[:300]
                    if len(err) > 300:
                        short_err += "...（已截断）"
                    report_lines.append(f"  - {short_err}")
            report_lines.append("")

    # 警告页面
    warning_pages = [(k, v) for k, v in results["pages"].items() if v["status"] == "warning"]
    if warning_pages:
        report_lines.append("---")
        report_lines.append("")
        report_lines.append("## 七、警告页面")
        report_lines.append("")
        for key, p in warning_pages:
            report_lines.append(f"### ⚠️ `{p.get('route', p['url'])}`")
            report_lines.append("")
            report_lines.append(f"- **分类**: {p['category']}")
            if p.get("warnings"):
                for w in p["warnings"]:
                    report_lines.append(f"  - {w}")
            report_lines.append("")

    # 失败的功能检查
    failed_func = [c for c in results["functional_checks"] if not c["passed"]]
    failed_sec = [c for c in results["security_checks"] if not c["passed"]]
    if failed_func or failed_sec:
        report_lines.append("---")
        report_lines.append("")
        report_lines.append("## 八、未通过的功能/安全检查")
        report_lines.append("")
        if failed_func:
            report_lines.append("### 功能检查失败项")
            report_lines.append("")
            for c in failed_func:
                report_lines.append(f"- **{c['name']}**")
                report_lines.append(f"  - 期望: {c['expected']}")
                report_lines.append(f"  - 实际: {c['actual']}")
            report_lines.append("")
        if failed_sec:
            report_lines.append("### 安全检查失败项")
            report_lines.append("")
            for c in failed_sec:
                report_lines.append(f"- **{c['name']}**")
                report_lines.append(f"  - 期望: {c['expected']}")
                report_lines.append(f"  - 实际: {c['actual']}")
            report_lines.append("")

    # 测试覆盖范围说明
    report_lines.append("---")
    report_lines.append("")
    report_lines.append("## 九、测试覆盖范围")
    report_lines.append("")
    report_lines.append("### 9.1 家长端路由（来自 `src/modules/layout/config/navigation.ts`）")
    report_lines.append("")
    report_lines.append("- `/parent/dashboard` - 家长仪表盘")
    report_lines.append("- `/parent/grades` - 子女成绩聚合页")
    report_lines.append("- `/parent/attendance` - 子女考勤聚合页")
    report_lines.append("- `/parent/children/[studentId]` - 单个子女详情页")
    report_lines.append("- `/announcements` - 公告列表（家长有 `ANNOUNCEMENT_READ` 权限）")
    report_lines.append("- `/messages` - 消息列表（家长有 `MESSAGE_READ` 权限）")
    report_lines.append("- `/messages/compose` - 写消息")
    report_lines.append("- `/profile` - 个人资料")
    report_lines.append("- `/settings` - 设置")
    report_lines.append("- `/settings/security` - 安全设置")
    report_lines.append("")
    report_lines.append("### 9.2 跨角色访问保护测试")
    report_lines.append("")
    report_lines.append("家长账号尝试访问以下路由，应被 `src/proxy.ts` 重定向回 `/parent/dashboard`：")
    report_lines.append("- `/admin/*` - 管理员页面（需 `SCHOOL_MANAGE` 权限）")
    report_lines.append("- `/teacher/*` - 教师页面（需 `EXAM_READ` 权限，家长虽有此权限但路由前缀仍会拦截教师专属页面）")
    report_lines.append("- `/student/*` - 学生页面（需 `HOMEWORK_SUBMIT` 权限）")
    report_lines.append("- `/management/*` - 管理页面（需 `GRADE_MANAGE` 权限）")
    report_lines.append("")
    report_lines.append("### 9.3 功能完整性检查项")
    report_lines.append("")
    report_lines.append("- 仪表盘：标题、问候语、快捷入口（Grades/Attendance/Announcements）、子女卡片、统计计数")
    report_lines.append("- 子女详情页：返回按钮、姓名标题、邮箱掩码、作业摘要、成绩趋势、今日课表、View all 链接")
    report_lines.append("- 侧边栏导航：仅显示家长相关菜单，不显示教师/管理员菜单")
    report_lines.append("- 跨家庭隔离：访问非关联子女应被拒绝")
    report_lines.append("")
    report_lines.append("---")
    report_lines.append("")
    report_lines.append(f"*报告自动生成于 {results['test_date']}*")

    return "\n".join(report_lines)


def main():
    # 运行测试
    run_all_tests()

    # 生成报告
    report = generate_report()

    # 写入文件
    output_dir = os.path.join(os.path.dirname(__file__), "..", "..", "bugs")
    os.makedirs(output_dir, exist_ok=True)
    output_path = os.path.join(output_dir, "parent_web_test.md")

    with open(output_path, "w", encoding="utf-8") as f:
        f.write(report)

    print(f"\n📄 报告已写入: {output_path}")

    # 同时输出JSON便于调试
    json_path = output_path.replace(".md", ".json")
    with open(json_path, "w", encoding="utf-8") as f:
        json.dump(results, f, ensure_ascii=False, indent=2, default=str)
    print(f"📄 JSON数据已写入: {json_path}")


if __name__ == "__main__":
    main()
