"""
考勤模块全功能 Web 测试脚本
使用 Playwright 对所有角色的考勤页面与核心交互进行功能测试
覆盖：admin / teacher / student / parent 四种角色
结果输出：webtest/attendance_0.1.0.md 与 webtest/attendance_0.1.0.json
"""
import json
import re
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 = "0.1.0"

# 测试账号（来自 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"},
}

# 各角色考勤页面路由（基于 src/modules/layout/config/navigation.ts 与源码目录）
ATTENDANCE_ROUTES = {
    "admin": [
        {"route": "/admin/attendance", "category": "考勤总览", "expected_keys": ["考勤总览"]},
    ],
    "teacher": [
        {"route": "/teacher/attendance", "category": "考勤记录", "expected_keys": ["考勤记录"]},
        {"route": "/teacher/attendance/sheet", "category": "录入考勤", "expected_keys": ["录入考勤"]},
        {"route": "/teacher/attendance/stats", "category": "考勤统计", "expected_keys": ["考勤统计"]},
    ],
    "student": [
        {"route": "/student/attendance", "category": "我的考勤", "expected_keys": ["我的考勤"]},
    ],
    "parent": [
        {"route": "/parent/attendance", "category": "子女考勤", "expected_keys": ["子女考勤"]},
    ],
}

# 跨角色访问保护测试：每个角色不应能访问其他角色的考勤页
# 注意：admin 拥有所有权限，可以访问所有路由，因此不测试 admin 的跨角色访问
CROSS_ROLE_FORBIDDEN = {
    "teacher": ["/admin/attendance", "/student/attendance", "/parent/attendance"],
    "student": ["/admin/attendance", "/teacher/attendance", "/parent/attendance"],
    "parent": ["/admin/attendance", "/teacher/attendance", "/student/attendance"],
}

PROJECT_ROOT = Path(__file__).resolve().parents[1]
WEBTEST_DIR = PROJECT_ROOT / "webtest"
SCREENSHOT_DIR = WEBTEST_DIR / "screenshots" / "attendance"
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": "考勤 (Attendance)",
    "version": VERSION,
    "base_url": BASE_URL,
    "summary": {
        "total": 0,
        "passed": 0,
        "failed": 0,
        "warnings": 0,
    },
    "roles": {},
    "cross_role_tests": [],
    "interactions": [],
    "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 login(page, role: str) -> bool:
    """登录指定角色账号"""
    account = TEST_ACCOUNTS[role]
    print(f"\n>>> 登录 {role} 账号 ({account['email']})...")

    # 先清除 cookies 确保干净状态
    page.context.clear_cookies()

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

    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"])

        # 点击登录按钮 - shadcn Button 不显式设置 type="submit"
        # 按钮文本是 "Sign In with Email"
        login_btn = page.get_by_role("button", name="Sign In with Email")
        if login_btn.count() == 0:
            login_btn = page.locator("form button").filter(has_text="Sign In")
        if login_btn.count() == 0:
            # 最后兜底：表单内第一个 button
            login_btn = page.locator("form button").first

        print(f"    找到登录按钮: {login_btn.count()} 个")
        login_btn.first.click()

        # 登录表单使用 signIn(redirect: false) + router.push
        # 需要等待 URL 变化，而不只是 networkidle
        try:
            page.wait_for_url(lambda url: "/login" not in url, timeout=20000)
        except PlaywrightTimeout:
            # 如果 URL 没变，可能登录失败
            pass

        page.wait_for_timeout(1500)
        print(f"    登录后 URL: {page.url}")

        if "/login" in page.url:
            # 重试一次
            print(f"    ⚠️ 首次登录失败，重试...")
            page.context.clear_cookies()
            page.goto(f"{BASE_URL}/login", timeout=30000)
            page.wait_for_load_state("networkidle", timeout=15000)
            page.wait_for_timeout(800)

            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.get_by_role("button", name="Sign In with Email")
            if login_btn.count() == 0:
                login_btn = page.locator("form button").filter(has_text="Sign In")
            if login_btn.count() == 0:
                login_btn = page.locator("form button").first
            login_btn.first.click()

            try:
                page.wait_for_url(lambda url: "/login" not in url, timeout=20000)
            except PlaywrightTimeout:
                pass
            page.wait_for_timeout(1500)
            print(f"    重试后 URL: {page.url}")

        if "/login" in page.url:
            print(f"    ❌ {role} 登录失败")
            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)

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


def test_role_attendance(page, role: str) -> dict:
    """测试单个角色的考勤页面"""
    routes = ATTENDANCE_ROUTES[role]
    role_result = {
        "role": role,
        "login_success": False,
        "pages": [],
        "interactions": [],
        "errors": [],
        "warnings": [],
    }

    print(f"\n=== 测试 {role} 考勤模块 ===")

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

    console_errors, on_console = collect_console_errors(page)

    # 测试每个页面
    for route_info in routes:
        route = route_info["route"]
        category = route_info["category"]
        expected_keys = route_info["expected_keys"]
        url = f"{BASE_URL}{route}"

        page_result = {
            "url": url,
            "route": route,
            "category": category,
            "status": "unknown",
            "http_status": None,
            "final_url": None,
            "checks": [],
            "errors": [],
            "warnings": [],
            "console_errors": [],
            "screenshot": None,
        }

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

        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

            # 截图
            safe_name = re.sub(r"[^\w\-]", "_", route.strip("/"))
            shot_path = SCREENSHOT_DIR / f"{role}_{safe_name}.png"
            try:
                page.screenshot(path=str(shot_path), full_page=True)
                page_result["screenshot"] = str(shot_path.relative_to(PROJECT_ROOT))
            except Exception as e:
                page_result["warnings"].append(f"截图失败: {e}")

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

            # HTTP 状态检查
            if http_status and http_status >= 500:
                page_result["status"] = "failed"
                page_result["errors"].append(f"HTTP {http_status} 服务器错误")
            elif http_status and http_status >= 400:
                page_result["status"] = "failed"
                page_result["errors"].append(f"HTTP {http_status} 客户端错误")
            elif _is_login_redirect(final_url):
                page_result["status"] = "failed"
                page_result["errors"].append("重定向到登录页 - 认证失败")
            elif urlparse(final_url).path != route:
                page_result["status"] = "warning"
                page_result["warnings"].append(f"重定向到 {final_url}（预期 {route}）")
            elif len(body_text.strip()) < 50:
                page_result["status"] = "warning"
                page_result["warnings"].append(f"页面内容过少（{len(body_text.strip())} 字符）")
            else:
                page_result["status"] = "passed"

            # 检查页面标题关键词
            title_matched = any(key in body_text for key in expected_keys)
            page_result["checks"].append({
                "name": f"标题关键词匹配 ({'/'.join(expected_keys)})",
                "passed": title_matched,
                "detail": f"实际匹配: {title_matched}"
            })
            if not title_matched:
                page_result["warnings"].append(f"未找到标题关键词: {expected_keys}")

            # 检查页面错误提示
            error_alerts = page.locator('[role="alert"], .text-destructive, .text-red-500, .text-red-600').all()
            for alert in error_alerts[:3]:
                try:
                    text = (alert.text_content() or "").strip()[:150]
                    if text:
                        page_result["warnings"].append(f"页面告警文本: {text}")
                except Exception:
                    pass

            # 收集控制台错误
            if console_errors:
                page_result["console_errors"] = console_errors[:5]
                if page_result["status"] == "passed":
                    page_result["status"] = "warning"
                    page_result["warnings"].append(f"控制台错误 {len(console_errors)} 条")

            status_icon = {"passed": "✅", "warning": "⚠️", "failed": "❌"}.get(page_result["status"], "❓")
            print(f"    {status_icon} {page_result['status']} (HTTP {http_status}) -> {final_url}")

        except PlaywrightTimeout:
            page_result["status"] = "failed"
            page_result["errors"].append("页面加载超时 (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]}")

        role_result["pages"].append(page_result)

    # 测试角色特定交互
    role_result["interactions"] = test_role_interactions(page, role)

    # 收集全局控制台错误
    if console_errors:
        for err in console_errors[:10]:
            results["console_errors_global"].append({"role": role, "error": err})

    try:
        page.remove_listener("console", on_console)
    except Exception:
        pass

    return role_result


def test_role_interactions(page, role: str) -> list:
    """测试角色特定的交互功能"""
    interactions = []

    if role == "teacher":
        # 测试录入考勤页面交互
        print("\n  >>> 测试交互: 录入考勤页面")
        try:
            page.goto(f"{BASE_URL}/teacher/attendance/sheet", timeout=30000)
            page.wait_for_load_state("networkidle", timeout=15000)
            page.wait_for_timeout(1500)

            # 检查班级选择器
            class_select = page.locator('select, button[role="combobox"]').first
            class_select_exists = class_select.count() > 0
            interactions.append({
                "name": "录入考勤 - 班级选择器存在",
                "passed": class_select_exists,
                "detail": ""
            })

            # 检查日期选择器
            date_input = page.locator('input[type="date"], input[name="date"]').first
            date_input_exists = date_input.count() > 0
            interactions.append({
                "name": "录入考勤 - 日期选择器存在",
                "passed": date_input_exists,
                "detail": ""
            })

            # 检查"全部标记到场"按钮或类似快捷操作
            body_text = page.locator("body").text_content() or ""
            has_mark_all = "全部标记到场" in body_text or "Mark All" in body_text
            interactions.append({
                "name": "录入考勤 - 全部标记到场按钮",
                "passed": has_mark_all,
                "detail": ""
            })

        except Exception as e:
            interactions.append({
                "name": "录入考勤交互测试",
                "passed": False,
                "detail": f"异常: {str(e)[:200]}"
            })

        # 测试考勤统计页面
        print("\n  >>> 测试交互: 考勤统计页面")
        try:
            page.goto(f"{BASE_URL}/teacher/attendance/stats", timeout=30000)
            page.wait_for_load_state("networkidle", timeout=15000)
            page.wait_for_timeout(1500)

            body_text = page.locator("body").text_content() or ""
            # 检查统计卡片
            has_stats = "出勤率" in body_text or "总记录数" in body_text or "Attendance" in body_text
            interactions.append({
                "name": "考勤统计 - 统计卡片显示",
                "passed": has_stats,
                "detail": ""
            })

            # 检查班级切换器（ChipNav 渲染为 Link，不是 button/tab）
            # 班级切换器链接指向 /teacher/attendance/stats?classId=
            class_selector = page.locator('a[href*="/teacher/attendance/stats?classId="]')
            class_selector_exists = class_selector.count() > 0
            interactions.append({
                "name": "考勤统计 - 班级切换器存在",
                "passed": class_selector_exists,
                "detail": f"找到 {class_selector.count()} 个班级切换链接"
            })

        except Exception as e:
            interactions.append({
                "name": "考勤统计交互测试",
                "passed": False,
                "detail": f"异常: {str(e)[:200]}"
            })

    elif role == "admin":
        # 测试管理员考勤总览
        print("\n  >>> 测试交互: 管理员考勤总览")
        try:
            page.goto(f"{BASE_URL}/admin/attendance", timeout=30000)
            page.wait_for_load_state("networkidle", timeout=15000)
            page.wait_for_timeout(1500)

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

            # 检查统计卡片
            has_stats = "出勤" in body_text or "缺勤" in body_text or "Attendance" in body_text
            interactions.append({
                "name": "管理员考勤总览 - 统计卡片显示",
                "passed": has_stats,
                "detail": ""
            })

            # 检查筛选器
            filter_exists = page.locator('select, [role="combobox"]').count() > 0
            interactions.append({
                "name": "管理员考勤总览 - 筛选器存在",
                "passed": filter_exists,
                "detail": ""
            })

            # 检查"统计分析"快捷链接
            stats_link = page.locator('a[href="/teacher/attendance/stats"]').count() > 0
            interactions.append({
                "name": "管理员考勤总览 - 统计分析快捷链接",
                "passed": stats_link,
                "detail": ""
            })

        except Exception as e:
            interactions.append({
                "name": "管理员考勤总览交互测试",
                "passed": False,
                "detail": f"异常: {str(e)[:200]}"
            })

    elif role == "student":
        # 测试学生考勤视图
        print("\n  >>> 测试交互: 学生考勤视图")
        try:
            page.goto(f"{BASE_URL}/student/attendance", timeout=30000)
            page.wait_for_load_state("networkidle", timeout=15000)
            page.wait_for_timeout(1500)

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

            # 检查统计信息
            has_stats = "出勤" in body_text or "缺勤" in body_text or "迟到" in body_text
            interactions.append({
                "name": "学生考勤 - 统计信息显示",
                "passed": has_stats,
                "detail": ""
            })

            # 检查最近记录
            has_records = "最近记录" in body_text or "Recent" in body_text or "记录" in body_text
            interactions.append({
                "name": "学生考勤 - 最近记录显示",
                "passed": has_records,
                "detail": ""
            })

        except Exception as e:
            interactions.append({
                "name": "学生考勤交互测试",
                "passed": False,
                "detail": f"异常: {str(e)[:200]}"
            })

    elif role == "parent":
        # 测试家长考勤视图
        print("\n  >>> 测试交互: 家长考勤视图")
        try:
            page.goto(f"{BASE_URL}/parent/attendance", timeout=30000)
            page.wait_for_load_state("networkidle", timeout=15000)
            page.wait_for_timeout(1500)

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

            # 检查子女考勤信息
            has_child_info = "子女" in body_text or "Child" in body_text or "考勤" in body_text
            interactions.append({
                "name": "家长考勤 - 子女信息显示",
                "passed": has_child_info,
                "detail": ""
            })

            # 检查出勤率卡片
            has_rate_card = "出勤率" in body_text or "Attendance Rate" in body_text
            interactions.append({
                "name": "家长考勤 - 出勤率卡片",
                "passed": has_rate_card,
                "detail": ""
            })

            # 检查月历视图
            has_calendar = "月历" in body_text or "Calendar" in body_text
            interactions.append({
                "name": "家长考勤 - 月历视图",
                "passed": has_calendar,
                "detail": ""
            })

        except Exception as e:
            interactions.append({
                "name": "家长考勤交互测试",
                "passed": False,
                "detail": f"异常: {str(e)[:200]}"
            })

    return interactions


def test_cross_role_access(page, role: str) -> list:
    """测试跨角色访问保护"""
    forbidden_routes = CROSS_ROLE_FORBIDDEN[role]
    test_results = []

    print(f"\n=== 测试 {role} 跨角色访问保护 ===")

    for route in forbidden_routes:
        result = {
            "role": role,
            "forbidden_route": route,
            "final_url": None,
            "passed": False,
            "error": None,
        }

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

            final_url = page.url
            final_path = urlparse(final_url).path
            result["final_url"] = final_url

            if _is_login_redirect(final_url):
                result["passed"] = True
                result["error"] = "重定向到登录页（拒绝访问）"
                print(f"    ✅ {route} -> 登录页（拒绝）")
            elif final_path.startswith(TEST_ACCOUNTS[role]["expected_path"]):
                result["passed"] = True
                result["error"] = f"重定向回 {final_path}（拒绝访问）"
                print(f"    ✅ {route} -> {final_path}（拒绝）")
            elif final_path == route:
                result["passed"] = False
                result["error"] = "成功访问禁止路由（权限漏洞）"
                print(f"    ❌ {route} -> 直接访问（权限漏洞）")
            elif "/403" in final_url or "/401" in final_url or "/unauthorized" in final_url:
                result["passed"] = True
                result["error"] = "重定向到未授权页"
                print(f"    ✅ {route} -> 未授权页（拒绝）")
            else:
                result["passed"] = True
                result["error"] = f"重定向到 {final_path}（拒绝访问）"
                print(f"    ✅ {route} -> {final_path}（拒绝）")
        except PlaywrightTimeout:
            result["error"] = "页面加载超时"
            result["passed"] = True
            print(f"    ⚠️ {route} -> 超时")
        except Exception as e:
            result["error"] = f"异常: {str(e)[:200]}"
            result["passed"] = True
            print(f"    ⚠️ {route} -> 异常")

        test_results.append(result)

    return test_results


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()

        for role in ["admin", "teacher", "student", "parent"]:
            logout(page)
            role_result = test_role_attendance(page, role)
            results["roles"][role] = role_result

            # admin 拥有所有权限，可以访问所有路由，跳过跨角色测试
            if role in CROSS_ROLE_FORBIDDEN:
                cross_role_results = test_cross_role_access(page, role)
                results["cross_role_tests"].extend(cross_role_results)

        # 汇总
        total = 0
        passed = 0
        failed = 0
        warnings = 0

        for role, r in results["roles"].items():
            for page_result in r.get("pages", []):
                total += 1
                if page_result["status"] == "passed":
                    passed += 1
                elif page_result["status"] == "warning":
                    warnings += 1
                else:
                    failed += 1
            for interaction in r.get("interactions", []):
                total += 1
                if interaction["passed"]:
                    passed += 1
                else:
                    failed += 1

        for r in results["cross_role_tests"]:
            total += 1
            if r["passed"]:
                passed += 1
            else:
                failed += 1

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

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

        browser.close()


def generate_report() -> str:
    """生成 Markdown 测试报告"""
    lines = []
    lines.append("# 考勤模块 Web 功能测试报告")
    lines.append("")
    lines.append(f"> 模块：考勤 (Attendance)")
    lines.append(f"> 版本：{results['version']}")
    lines.append(f"> 测试日期：{results['test_date']}")
    lines.append(f"> 测试工具：Playwright + Chromium (headless)")
    lines.append(f"> Base URL：{results['base_url']}")
    lines.append(f"> 测试范围：admin / teacher / student / parent 四角色考勤页面 + 跨角色访问保护 + 关键交互")
    lines.append("")
    lines.append("---")
    lines.append("")

    # 测试概览
    lines.append("## 一、测试概览")
    lines.append("")
    s = results["summary"]
    pass_rate = (s["passed"] / s["total"] * 100) if s["total"] > 0 else 0
    lines.append("| 指标 | 数值 |")
    lines.append("|------|------|")
    lines.append(f"| 总测试项 | {s['total']} |")
    lines.append(f"| 通过 | {s['passed']} |")
    lines.append(f"| 失败 | {s['failed']} |")
    lines.append(f"| 警告 | {s['warnings']} |")
    lines.append(f"| 通过率 | {pass_rate:.1f}% |")
    lines.append("")
    lines.append("---")
    lines.append("")

    # 测试账号
    lines.append("## 二、测试账号")
    lines.append("")
    lines.append("| 角色 | 邮箱 | 预期仪表盘路径 |")
    lines.append("|------|------|----------------|")
    for role, acc in TEST_ACCOUNTS.items():
        lines.append(f"| {role} | `{acc['email']}` | `{acc['expected_path']}` |")
    lines.append("")
    lines.append("---")
    lines.append("")

    # 各角色测试详情
    lines.append("## 三、各角色测试详情")
    lines.append("")
    for role in ["admin", "teacher", "student", "parent"]:
        r = results["roles"].get(role, {})
        if not r:
            continue
        lines.append(f"### {role.upper()} 考勤模块")
        lines.append("")
        lines.append(f"- **登录**: {'✅ 成功' if r.get('login_success') else '❌ 失败'}")
        if r.get("errors"):
            lines.append(f"- **错误**:")
            for err in r["errors"]:
                lines.append(f"  - {err}")
        if r.get("warnings"):
            lines.append(f"- **警告**:")
            for w in r["warnings"]:
                lines.append(f"  - {w}")
        lines.append("")

        # 页面测试详情
        if r.get("pages"):
            lines.append("#### 页面测试")
            lines.append("")
            lines.append("| 状态 | 类别 | 路由 | HTTP | 最终 URL | 错误 | 警告 |")
            lines.append("|------|------|------|------|----------|------|------|")
            for p in r["pages"]:
                icon = {"passed": "✅", "warning": "⚠️", "failed": "❌"}.get(p["status"], "❓")
                errs = "; ".join(p.get("errors", []))[:80] or "-"
                warns = "; ".join(p.get("warnings", []))[:80] or "-"
                lines.append(f"| {icon} | {p['category']} | `{p['route']}` | {p.get('http_status', '-')} | `{p.get('final_url', '-')}` | {errs} | {warns} |")
            lines.append("")

            # 检查项明细
            for p in r["pages"]:
                if p.get("checks"):
                    lines.append(f"##### 检查项明细 - {p['category']}")
                    lines.append("")
                    lines.append("| 状态 | 检查项 | 详情 |")
                    lines.append("|------|--------|------|")
                    for c in p["checks"]:
                        icon = "✅" if c["passed"] else "❌"
                        lines.append(f"| {icon} | {c['name']} | {c.get('detail', '')} |")
                    lines.append("")

        # 交互测试
        if r.get("interactions"):
            lines.append("#### 交互测试")
            lines.append("")
            lines.append("| 状态 | 交互项 | 详情 |")
            lines.append("|------|--------|------|")
            for it in r["interactions"]:
                icon = "✅" if it["passed"] else "❌"
                lines.append(f"| {icon} | {it['name']} | {it.get('detail', '')} |")
            lines.append("")

        lines.append("---")
        lines.append("")

    # 跨角色访问保护测试
    lines.append("## 四、跨角色访问保护测试")
    lines.append("")
    lines.append("| 角色 | 禁止路由 | 实际 URL | 结果 | 说明 |")
    lines.append("|------|----------|----------|------|------|")
    for r in results["cross_role_tests"]:
        icon = "✅" if r["passed"] else "❌"
        actual_path = urlparse(r.get("final_url", "")).path or "-"
        lines.append(f"| {r['role']} | `{r['forbidden_route']}` | `{actual_path}` | {icon} {'拒绝' if r['passed'] else '通过'} | {r.get('error', '-') or '-'} |")
    lines.append("")
    lines.append("---")
    lines.append("")

    # 失败项汇总
    failed_items = []
    for role, r in results["roles"].items():
        for p in r.get("pages", []):
            if p["status"] == "failed":
                failed_items.append(("页面测试", role, p.get("route", ""), p.get("errors", [])))
        for it in r.get("interactions", []):
            if not it["passed"]:
                failed_items.append(("交互测试", role, it["name"], [it.get("detail", "")]))
    for r in results["cross_role_tests"]:
        if not r["passed"]:
            failed_items.append(("跨角色保护", r["role"], r.get("forbidden_route", ""), [r.get("error", "")]))

    if failed_items:
        lines.append("## 五、失败项汇总")
        lines.append("")
        lines.append("| 类别 | 角色 | 路径/项 | 错误 |")
        lines.append("|------|------|---------|------|")
        for cat, role, item, errs in failed_items:
            err_str = "; ".join(errs[:2]) if errs else "-"
            lines.append(f"| {cat} | {role} | `{item}` | {err_str} |")
        lines.append("")
        lines.append("---")
        lines.append("")

    # 结论与建议
    lines.append("## 六、测试结论与改进建议")
    lines.append("")
    if s["failed"] == 0:
        lines.append("✅ **考勤模块所有测试通过**，各角色考勤页面功能正常，权限保护有效。")
    else:
        lines.append(f"❌ **{s['failed']} 项测试失败**，需修复以下问题：")
        lines.append("")
        for cat, role, item, errs in failed_items:
            lines.append(f"- **{cat} - {role}** (`{item}`): {'; '.join(errs[:2]) if errs else '未知错误'}")
    lines.append("")
    lines.append("### 改进建议")
    lines.append("")
    lines.append("1. **认证与权限**：失败页面中若出现重定向至 /login，需检查会话过期策略与权限校验逻辑。")
    lines.append("2. **HTTP 5xx 错误**：服务端错误需检查 Server Action 数据访问层与数据库连接。")
    lines.append("3. **页面内容为空**：检查数据查询条件与渲染逻辑，确认数据源是否返回预期结果。")
    lines.append("4. **控制台错误**：浏览器控制台报错需检查前端组件渲染与 API 调用。")
    lines.append("5. **跨角色访问**：若出现权限漏洞，需检查 `requirePermission()` 调用与角色-权限映射。")
    lines.append("6. **i18n 缺失**：若页面出现英文硬编码，需补充对应 i18n key。")
    lines.append("")
    lines.append("---")
    lines.append("")
    lines.append(f"*报告自动生成于 {results['test_date']}*")

    return "\n".join(lines)


def main():
    run_all_tests()
    report = generate_report()

    output_path = WEBTEST_DIR / f"attendance_{VERSION}.md"
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(report)
    print(f"\n📄 报告已写入: {output_path}")

    json_path = output_path.parent / f"attendance_{VERSION}.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()
