"""
班级管理模块（class-management）全功能 Web 测试脚本
使用 Playwright 对所有角色的班级管理模块访问与功能进行测试

覆盖模块：
- 班级管理(admin) (/admin/school/classes) - SCHOOL_MANAGE 权限（admin）
- 年级班级管理 (/management/grade/classes) - GRADE_MANAGE 权限（grade_head/teaching_head）
- 教师班级 (/teacher/classes) - CLASS_READ 权限（teacher）
- 教师我的班级 (/teacher/classes/my) - CLASS_READ 权限（teacher）

覆盖角色：
- admin（完整 CRUD on /admin/school/classes）
- teacher（可查看 /teacher/classes/my，作为 grade_head 可访问 /management/grade/classes）
- student（应被拒绝访问管理页面）
- parent（应被拒绝访问管理页面）

结果输出：webtest/class-management_v1.md 与 webtest/class-management_v1.json
"""
import json
import os
import re
import sys
import time
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 = "class-management"

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

# 各角色对班级管理模块的预期访问行为
# admin: 同时拥有 SCHOOL_MANAGE、GRADE_MANAGE、EXAM_CREATE，可访问所有班级管理路由
# teacher: 拥有 EXAM_CREATE 和 GRADE_MANAGE（作为 grade_head），可访问 /management/* 和 /teacher/*
# student/parent: 都不能访问管理路由和教师路由
ROLE_EXPECTATIONS = {
    "admin": {
        "admin_classes": {"can_access": True, "can_create": True, "can_edit": True, "can_delete": True},
        "management_classes": {"can_access": True, "can_create": True, "can_edit": True, "can_delete": True},
        "teacher_classes": {"can_access": True, "can_create": False, "can_edit": False, "can_delete": False},
        "teacher_my_classes": {"can_access": True, "can_create": False, "can_edit": False, "can_delete": False},
    },
    "teacher": {
        "admin_classes": {"can_access": False, "expected_redirect_reason": "forbidden"},
        "management_classes": {"can_access": True, "can_create": True, "can_edit": True, "can_delete": True},
        "teacher_classes": {"can_access": True, "can_create": False, "can_edit": False, "can_delete": False},
        "teacher_my_classes": {"can_access": True, "can_create": False, "can_edit": False, "can_delete": False},
    },
    "student": {
        "admin_classes": {"can_access": False, "expected_redirect_reason": "forbidden"},
        "management_classes": {"can_access": False, "expected_redirect_reason": "forbidden"},
        "teacher_classes": {"can_access": False, "expected_redirect_reason": "forbidden"},
        "teacher_my_classes": {"can_access": False, "expected_redirect_reason": "forbidden"},
    },
    "parent": {
        "admin_classes": {"can_access": False, "expected_redirect_reason": "forbidden"},
        "management_classes": {"can_access": False, "expected_redirect_reason": "forbidden"},
        "teacher_classes": {"can_access": False, "expected_redirect_reason": "forbidden"},
        "teacher_my_classes": {"can_access": False, "expected_redirect_reason": "forbidden"},
    },
}

# 班级管理模块路由
CLASS_ROUTES = {
    "admin_classes": "/admin/school/classes",
    "management_classes": "/management/grade/classes",
    "teacher_classes": "/teacher/classes",
    "teacher_my_classes": "/teacher/classes/my",
}

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": "班级管理 (Class Management)",
    "version": VERSION,
    "base_url": BASE_URL,
    "covered_submodules": ["admin-classes", "management-classes", "teacher-classes", "teacher-my-classes"],
    "summary": {
        "total": 0,
        "passed": 0,
        "failed": 0,
        "warnings": 0,
    },
    "roles": {},
    "console_errors_global": [],
}


def _is_login_redirect(url: str) -> bool:
    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:
    parsed = urlparse(url)
    query = parse_qs(parsed.query)
    return query.get("reason", [""])[0] == "forbidden"


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

        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(2000)

        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 complete_parent_onboarding(page, role: str) -> bool:
    print(f"    >>> 完成 {role} onboarding...")
    try:
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1000)

        if "/onboarding" not in page.url:
            return True

        page.wait_for_selector('[role="dialog"]', timeout=10000)
        page.wait_for_timeout(500)

        next_btn = page.locator('button:has-text("下一步"), button:has-text("Next")').first
        if next_btn.count() > 0:
            next_btn.click()
            page.wait_for_timeout(500)

        name_input = page.locator('input[id="onb_name"]')
        if name_input.count() == 0:
            name_input = page.locator('input').nth(0)
        if name_input.count() > 0:
            name_input.fill("测试家长")

        phone_input = page.locator('input[id="onb_phone"]')
        if phone_input.count() == 0:
            phone_input = page.locator('input').nth(1)
        if phone_input.count() > 0:
            phone_input.fill("13800000000")

        address_input = page.locator('input[id="onb_address"]')
        if address_input.count() == 0:
            address_input = page.locator('input').nth(2)
        if address_input.count() > 0:
            address_input.fill("测试地址")

        next_btn = page.locator('button:has-text("下一步"), button:has-text("Next")').first
        if next_btn.count() > 0:
            next_btn.click()
            page.wait_for_timeout(500)

        skip_btn = page.locator('button:has-text("跳过"), button:has-text("Skip")').first
        if skip_btn.count() > 0:
            skip_btn.click()
            page.wait_for_timeout(500)
        else:
            next_btn = page.locator('button:has-text("下一步"), button:has-text("Next")').first
            if next_btn.count() > 0:
                next_btn.click()
                page.wait_for_timeout(500)

        finish_btn = page.locator('button:has-text("完成"), button:has-text("Finish")').first
        if finish_btn.count() > 0:
            finish_btn.click()
            page.wait_for_load_state("networkidle", timeout=15000)
            page.wait_for_timeout(2000)

        return "/onboarding" not in page.url
    except Exception as e:
        print(f"    onboarding 异常: {e}")
        return False


def collect_console_errors(page):
    errors = []

    def on_console(msg):
        if msg.type == "error":
            errors.append(msg.text)

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


def test_page_access(page, role: str, route_key: str, route: str) -> dict:
    url = f"{BASE_URL}{route}"
    expectation = ROLE_EXPECTATIONS[role][route_key]

    result = {
        "route_key": route_key,
        "url": url,
        "expected_access": expectation["can_access"],
        "http_status": None,
        "final_url": None,
        "status": "unknown",
        "errors": [],
        "warnings": [],
        "checks": [],
    }

    print(f"\n  测试访问: {route_key} ({route}) as {role}")

    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
        result["http_status"] = http_status
        result["final_url"] = final_url

        screenshot_name = f"{route_key}_{role}.png"
        try:
            page.screenshot(path=str(SCREENSHOT_DIR / screenshot_name), full_page=True)
        except Exception:
            pass

        if http_status and http_status >= 500:
            result["status"] = "failed"
            result["errors"].append(f"HTTP {http_status} 服务器错误")
        elif _is_login_redirect(final_url):
            result["status"] = "failed"
            result["errors"].append("重定向到登录页 - 认证失败")
        elif expectation["can_access"]:
            if urlparse(final_url).path == route:
                result["status"] = "passed"
                result["checks"].append({"name": "页面正常加载", "passed": True})
            else:
                result["status"] = "warning"
                result["warnings"].append(f"重定向到 {final_url}（预期 {route}）")
        else:
            if _is_forbidden_redirect(final_url):
                result["status"] = "passed"
                result["checks"].append({
                    "name": "权限拒绝重定向",
                    "passed": True,
                    "detail": f"正确重定向到 {final_url}"
                })
            elif urlparse(final_url).path != route:
                result["status"] = "passed"
                result["checks"].append({
                    "name": "权限拒绝重定向",
                    "passed": True,
                    "detail": f"重定向到 {final_url}"
                })
            else:
                result["status"] = "failed"
                result["errors"].append(f"无权限角色 {role} 不应能访问 {route}")

        if result["status"] == "passed" and expectation["can_access"]:
            body_text = page.locator("body").text_content() or ""
            if len(body_text.strip()) < 50:
                result["warnings"].append("页面内容过少（<50 字符）")

    except PlaywrightTimeout:
        result["status"] = "failed"
        result["errors"].append("页面加载超时 (30s)")
    except Exception as e:
        result["status"] = "failed"
        result["errors"].append(f"异常: {str(e)[:200]}")

    status_icon = "✅" if result["status"] == "passed" else "⚠️" if result["status"] == "warning" else "❌"
    print(f"    {status_icon} {result['status']} (HTTP {result['http_status']})")
    return result


def test_admin_classes_crud(page, role: str) -> dict:
    """测试 admin 班级管理 CRUD（仅 admin）"""
    result = {
        "feature": "admin 班级 CRUD",
        "role": role,
        "operations": [],
        "status": "unknown",
        "errors": [],
    }

    if role != "admin":
        result["status"] = "skipped"
        result["errors"].append(f"{role} 无 SCHOOL_MANAGE 权限，跳过 admin 班级 CRUD 测试")
        return result

    route = CLASS_ROUTES["admin_classes"]
    url = f"{BASE_URL}{route}"

    try:
        page.goto(url, timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1500)

        # 1. 列表加载
        list_check = {"name": "列表加载", "passed": False, "detail": ""}
        try:
            table = page.locator("table")
            empty_state = page.locator('[class*="empty"]')
            if table.count() > 0 or empty_state.count() > 0:
                list_check["passed"] = True
                list_check["detail"] = "列表/空状态正常显示"
            else:
                list_check["detail"] = "未找到表格或空状态"
        except Exception as e:
            list_check["detail"] = f"异常: {e}"
        result["operations"].append(list_check)

        # 2. 打开创建对话框
        create_check = {"name": "打开创建对话框", "passed": False, "detail": ""}
        try:
            new_btn = page.locator('button:has-text("New"), button:has-text("新建"), button:has-text("Create"), button:has-text("新增")').first
            if new_btn.count() == 0:
                new_btn = page.locator('button:has(svg[class*="lucide-plus"])').first
            if new_btn.count() > 0:
                new_btn.click()
                page.wait_for_timeout(1000)
                dialog = page.locator('[role="dialog"]')
                if dialog.count() > 0:
                    create_check["passed"] = True
                    create_check["detail"] = "创建对话框已打开"

                    name_input = page.locator('input[name="name"]').first
                    if name_input.count() > 0:
                        name_input.fill(f"测试班级_{datetime.now().strftime('%H%M%S')}")

                    cancel_btn = page.locator('button:has-text("Cancel"), button:has-text("取消")').first
                    if cancel_btn.count() > 0:
                        cancel_btn.click()
                        page.wait_for_timeout(500)
                else:
                    create_check["detail"] = "对话框未打开"
            else:
                create_check["detail"] = "未找到新建按钮"
        except Exception as e:
            create_check["detail"] = f"异常: {e}"
        result["operations"].append(create_check)

        # 3. 编辑/删除入口
        for action_name, action_text in [("编辑入口存在", "Edit"), ("删除入口存在", "Delete")]:
            check = {"name": action_name, "passed": False, "detail": ""}
            try:
                menu_btn = page.locator('button:has(svg[class*="lucide-more-horizontal"])').first
                if menu_btn.count() > 0:
                    menu_btn.click()
                    page.wait_for_timeout(500)
                    cn_text = "编辑" if action_text == "Edit" else "删除"
                    item = page.locator(f'[role="menuitem"]:has-text("{action_text}"), [role="menuitem"]:has-text("{cn_text}")').first
                    if item.count() > 0:
                        check["passed"] = True
                        check["detail"] = f"{action_text} 菜单项存在"
                    else:
                        check["detail"] = f"未找到 {action_text} 菜单项"
                    page.keyboard.press("Escape")
                    page.wait_for_timeout(300)
                else:
                    check["detail"] = "无数据行，跳过"
                    check["passed"] = True
            except Exception as e:
                check["detail"] = f"异常: {e}"
            result["operations"].append(check)

        passed_ops = sum(1 for op in result["operations"] if op["passed"])
        if passed_ops == len(result["operations"]):
            result["status"] = "passed"
        elif passed_ops > 0:
            result["status"] = "warning"
        else:
            result["status"] = "failed"

    except Exception as e:
        result["status"] = "failed"
        result["errors"].append(f"CRUD 测试异常: {str(e)[:200]}")

    return result


def test_management_classes_crud(page, role: str) -> dict:
    """测试年级班级管理 CRUD（仅 teacher/grade_head）"""
    result = {
        "feature": "年级班级 CRUD (grade_head)",
        "role": role,
        "operations": [],
        "status": "unknown",
        "errors": [],
    }

    if role not in ("admin", "teacher"):
        result["status"] = "skipped"
        result["errors"].append(f"{role} 无 GRADE_MANAGE 权限，跳过 management 班级 CRUD 测试")
        return result

    route = CLASS_ROUTES["management_classes"]
    url = f"{BASE_URL}{route}"

    try:
        page.goto(url, timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1500)

        # 1. 列表加载
        list_check = {"name": "列表加载", "passed": False, "detail": ""}
        try:
            table = page.locator("table")
            empty_state = page.locator('[class*="empty"]')
            if table.count() > 0 or empty_state.count() > 0:
                list_check["passed"] = True
                list_check["detail"] = "列表/空状态正常显示"
            else:
                list_check["detail"] = "未找到表格或空状态"
        except Exception as e:
            list_check["detail"] = f"异常: {e}"
        result["operations"].append(list_check)

        # 2. 打开创建对话框
        create_check = {"name": "打开创建对话框", "passed": False, "detail": ""}
        try:
            new_btn = page.locator('button:has-text("New"), button:has-text("新建"), button:has-text("Create"), button:has-text("新增")').first
            if new_btn.count() == 0:
                new_btn = page.locator('button:has(svg[class*="lucide-plus"])').first
            if new_btn.count() > 0:
                new_btn.click()
                page.wait_for_timeout(1000)
                dialog = page.locator('[role="dialog"]')
                if dialog.count() > 0:
                    create_check["passed"] = True
                    create_check["detail"] = "创建对话框已打开"

                    name_input = page.locator('input[name="name"]').first
                    if name_input.count() > 0:
                        name_input.fill(f"测试班级_{datetime.now().strftime('%H%M%S')}")

                    cancel_btn = page.locator('button:has-text("Cancel"), button:has-text("取消")').first
                    if cancel_btn.count() > 0:
                        cancel_btn.click()
                        page.wait_for_timeout(500)
                else:
                    create_check["detail"] = "对话框未打开"
            else:
                create_check["detail"] = "未找到新建按钮"
        except Exception as e:
            create_check["detail"] = f"异常: {e}"
        result["operations"].append(create_check)

        # 3. 编辑/删除入口
        for action_name, action_text in [("编辑入口存在", "Edit"), ("删除入口存在", "Delete")]:
            check = {"name": action_name, "passed": False, "detail": ""}
            try:
                menu_btn = page.locator('button:has(svg[class*="lucide-more-horizontal"])').first
                if menu_btn.count() > 0:
                    menu_btn.click()
                    page.wait_for_timeout(500)
                    cn_text = "编辑" if action_text == "Edit" else "删除"
                    item = page.locator(f'[role="menuitem"]:has-text("{action_text}"), [role="menuitem"]:has-text("{cn_text}")').first
                    if item.count() > 0:
                        check["passed"] = True
                        check["detail"] = f"{action_text} 菜单项存在"
                    else:
                        check["detail"] = f"未找到 {action_text} 菜单项"
                    page.keyboard.press("Escape")
                    page.wait_for_timeout(300)
                else:
                    check["detail"] = "无数据行，跳过"
                    check["passed"] = True
            except Exception as e:
                check["detail"] = f"异常: {e}"
            result["operations"].append(check)

        passed_ops = sum(1 for op in result["operations"] if op["passed"])
        if passed_ops == len(result["operations"]):
            result["status"] = "passed"
        elif passed_ops > 0:
            result["status"] = "warning"
        else:
            result["status"] = "failed"

    except Exception as e:
        result["status"] = "failed"
        result["errors"].append(f"CRUD 测试异常: {str(e)[:200]}")

    return result


def test_teacher_classes_view(page, role: str) -> dict:
    """测试教师班级查看（admin 和 teacher）"""
    result = {
        "feature": "教师班级查看",
        "role": role,
        "operations": [],
        "status": "unknown",
        "errors": [],
    }

    if role not in ("admin", "teacher"):
        result["status"] = "skipped"
        result["errors"].append(f"{role} 无 CLASS_READ 权限，跳过教师班级查看测试")
        return result

    route = CLASS_ROUTES["teacher_my_classes"]
    url = f"{BASE_URL}{route}"

    try:
        page.goto(url, timeout=30000)
        page.wait_for_load_state("networkidle", timeout=15000)
        page.wait_for_timeout(1500)

        # 1. 页面加载
        load_check = {"name": "页面加载", "passed": False, "detail": ""}
        try:
            body_text = page.locator("body").text_content() or ""
            if len(body_text.strip()) > 50:
                load_check["passed"] = True
                load_check["detail"] = f"页面内容长度 {len(body_text.strip())}"
            else:
                load_check["detail"] = "页面内容过少"
        except Exception as e:
            load_check["detail"] = f"异常: {e}"
        result["operations"].append(load_check)

        # 2. 班级卡片或列表存在
        list_check = {"name": "班级卡片/列表存在", "passed": False, "detail": ""}
        try:
            # 教师我的班级页面通常显示班级卡片
            cards = page.locator('[class*="card"], [class*="grid"]').count()
            table = page.locator("table").count()
            empty_state = page.locator('[class*="empty"]').count()
            if cards > 0 or table > 0 or empty_state > 0:
                list_check["passed"] = True
                list_check["detail"] = f"找到卡片/表格/空状态（cards: {cards}, tables: {table}, empty: {empty_state}）"
            else:
                list_check["detail"] = "未找到班级卡片或列表"
        except Exception as e:
            list_check["detail"] = f"异常: {e}"
        result["operations"].append(list_check)

        passed_ops = sum(1 for op in result["operations"] if op["passed"])
        if passed_ops == len(result["operations"]):
            result["status"] = "passed"
        elif passed_ops > 0:
            result["status"] = "warning"
        else:
            result["status"] = "failed"

    except Exception as e:
        result["status"] = "failed"
        result["errors"].append(f"教师班级查看测试异常: {str(e)[:200]}")

    return result


def test_role(page, role: str) -> dict:
    role_result = {
        "role": role,
        "login_success": False,
        "access_tests": [],
        "crud_tests": [],
        "view_tests": [],
        "errors": [],
        "warnings": [],
    }

    print(f"\n{'='*60}")
    print(f"=== 测试角色: {role} ===")
    print(f"{'='*60}")

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

    if role == "parent" and "/onboarding" in page.url:
        if not complete_parent_onboarding(page, role):
            role_result["warnings"].append("onboarding 未完成")

    console_errors, on_console = collect_console_errors(page)

    # 测试每个路由的访问权限
    for route_key, route in CLASS_ROUTES.items():
        access_result = test_page_access(page, role, route_key, route)
        role_result["access_tests"].append(access_result)

    # 测试 CRUD
    role_result["crud_tests"].append(test_admin_classes_crud(page, role))
    role_result["crud_tests"].append(test_management_classes_crud(page, role))

    # 测试教师班级查看
    role_result["view_tests"].append(test_teacher_classes_view(page, role))

    if console_errors:
        role_result["warnings"].extend([f"控制台错误: {e[:200]}" for e in console_errors[:5]])
        results["console_errors_global"].extend([{f"role": role, "error": e} for e in console_errors[:5]])

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

    logout(page)
    return role_result


def run_all_tests():
    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"]:
            role_result = test_role(page, role)
            results["roles"][role] = role_result

        browser.close()

    total = 0
    passed = 0
    failed = 0
    warnings = 0

    for role, role_data in results["roles"].items():
        for access in role_data.get("access_tests", []):
            total += 1
            if access["status"] == "passed":
                passed += 1
            elif access["status"] == "warning":
                warnings += 1
            else:
                failed += 1
        for crud in role_data.get("crud_tests", []):
            if crud["status"] == "skipped":
                continue
            total += 1
            if crud["status"] == "passed":
                passed += 1
            elif crud["status"] == "warning":
                warnings += 1
            else:
                failed += 1
        for view in role_data.get("view_tests", []):
            if view["status"] == "skipped":
                continue
            total += 1
            if view["status"] == "passed":
                passed += 1
            elif view["status"] == "warning":
                warnings += 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}")


def generate_report() -> str:
    lines = []
    lines.append("# 班级管理模块 Web 功能测试报告")
    lines.append("")
    lines.append(f"> 测试日期：{results['test_date']}")
    lines.append(f"> 模块：{results['module']}")
    lines.append(f"> 版本：{results['version']}")
    lines.append(f"> 测试工具：Playwright + Chromium (headless)")
    lines.append(f"> Base URL：{results['base_url']}")
    lines.append(f"> 覆盖子模块：{', '.join(results['covered_submodules'])}")
    lines.append("")
    lines.append("---")
    lines.append("")
    lines.append("## 一、测试概览")
    lines.append("")
    s = results["summary"]
    lines.append("| 指标 | 数值 |")
    lines.append("|------|------|")
    lines.append(f"| 总测试项 | {s['total']} |")
    lines.append(f"| 通过 | {s['passed']} |")
    lines.append(f"| 失败 | {s['failed']} |")
    lines.append(f"| 警告 | {s['warnings']} |")
    if s["total"] > 0:
        lines.append(f"| 通过率 | {s['passed']/s['total']*100:.1f}% |")
    else:
        lines.append("| 通过率 | N/A |")
    lines.append("")
    lines.append("### 测试覆盖")
    lines.append("")
    lines.append("| 子模块 | 路由 | 权限点 | admin | teacher | student | parent |")
    lines.append("|--------|------|--------|-------|---------|---------|--------|")
    lines.append("| 班级管理(admin) | /admin/school/classes | SCHOOL_MANAGE | ✅ 完整CRUD | ❌ 拒绝 | ❌ 拒绝 | ❌ 拒绝 |")
    lines.append("| 年级班级管理 | /management/grade/classes | GRADE_MANAGE | ✅ 完整CRUD | ✅ 完整CRUD | ❌ 拒绝 | ❌ 拒绝 |")
    lines.append("| 教师班级 | /teacher/classes | EXAM_CREATE | ✅ 查看 | ✅ 查看 | ❌ 拒绝 | ❌ 拒绝 |")
    lines.append("| 教师我的班级 | /teacher/classes/my | EXAM_CREATE | ✅ 查看 | ✅ 查看 | ❌ 拒绝 | ❌ 拒绝 |")
    lines.append("")
    lines.append("---")
    lines.append("")

    lines.append("## 二、各角色测试详情")
    lines.append("")

    for role, role_data in results["roles"].items():
        lines.append(f"### 角色：{role}")
        lines.append("")
        lines.append(f"- **登录状态**: {'✅ 成功' if role_data['login_success'] else '❌ 失败'}")
        if role_data.get("errors"):
            for err in role_data["errors"]:
                lines.append(f"- **错误**: {err}")
        if role_data.get("warnings"):
            for w in role_data["warnings"]:
                lines.append(f"- **警告**: {w}")
        lines.append("")

        if role_data.get("access_tests"):
            lines.append("#### 访问权限测试")
            lines.append("")
            lines.append("| 状态 | 子模块 | 路由 | HTTP | 最终 URL | 备注 |")
            lines.append("|------|--------|------|------|----------|------|")
            for access in role_data["access_tests"]:
                status_icon = "✅" if access["status"] == "passed" else "⚠️" if access["status"] == "warning" else "❌"
                short_url = (access.get("final_url") or "").replace(BASE_URL, "")[:80]
                notes = []
                if access.get("errors"):
                    notes.append(f"错误: {'; '.join(access['errors'][:2])}")
                if access.get("warnings"):
                    notes.append(f"警告: {'; '.join(access['warnings'][:2])}")
                note_str = "<br>".join(notes) if notes else "-"
                route = access["route_key"]
                lines.append(f"| {status_icon} | {route} | `{access['url'].replace(BASE_URL, '')}` | {access['http_status'] or '-'} | `{short_url}` | {note_str} |")
            lines.append("")

        if role_data.get("crud_tests"):
            lines.append("#### CRUD 功能测试")
            lines.append("")
            lines.append("| 状态 | 功能 | 操作 | 详情 |")
            lines.append("|------|------|------|------|")
            for crud in role_data["crud_tests"]:
                if crud["status"] == "skipped":
                    lines.append(f"| ⏭️ | {crud['feature']} | 跳过 | {crud['errors'][0] if crud['errors'] else '-'} |")
                    continue
                status_icon = "✅" if crud["status"] == "passed" else "⚠️" if crud["status"] == "warning" else "❌"
                for op in crud.get("operations", []):
                    op_icon = "✅" if op["passed"] else "❌"
                    lines.append(f"| {status_icon} | {crud['feature']} | {op_icon} {op['name']} | {op['detail']} |")
            lines.append("")

        if role_data.get("view_tests"):
            lines.append("#### 查看功能测试")
            lines.append("")
            lines.append("| 状态 | 功能 | 操作 | 详情 |")
            lines.append("|------|------|------|------|")
            for view in role_data["view_tests"]:
                if view["status"] == "skipped":
                    lines.append(f"| ⏭️ | {view['feature']} | 跳过 | {view['errors'][0] if view['errors'] else '-'} |")
                    continue
                status_icon = "✅" if view["status"] == "passed" else "⚠️" if view["status"] == "warning" else "❌"
                for op in view.get("operations", []):
                    op_icon = "✅" if op["passed"] else "❌"
                    lines.append(f"| {status_icon} | {view['feature']} | {op_icon} {op['name']} | {op['detail']} |")
            lines.append("")

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

    failed_items = []
    for role, role_data in results["roles"].items():
        for access in role_data.get("access_tests", []):
            if access["status"] == "failed":
                failed_items.append({"role": role, "type": "访问", "detail": access})
        for crud in role_data.get("crud_tests", []):
            if crud["status"] == "failed":
                failed_items.append({"role": role, "type": "CRUD", "detail": crud})
        for view in role_data.get("view_tests", []):
            if view["status"] == "failed":
                failed_items.append({"role": role, "type": "查看", "detail": view})

    if failed_items:
        lines.append("## 三、失败项详情")
        lines.append("")
        for item in failed_items:
            d = item["detail"]
            lines.append(f"### ❌ [{item['role']}] {item['type']}: {d.get('feature', d.get('route_key', ''))}")
            lines.append("")
            if "url" in d:
                lines.append(f"- **URL**: {d['url']}")
            if "errors" in d and d["errors"]:
                for err in d["errors"]:
                    lines.append(f"- **错误**: {err}")
            if "operations" in d:
                for op in d["operations"]:
                    if not op["passed"]:
                        lines.append(f"- **操作失败**: {op['name']} - {op['detail']}")
            lines.append("")

    if results.get("console_errors_global"):
        lines.append("## 四、控制台错误汇总")
        lines.append("")
        for err in results["console_errors_global"][:20]:
            lines.append(f"- **[{err['role']}]** {err['error'][:200]}")
        lines.append("")

    lines.append("## 五、测试结论")
    lines.append("")
    if results["summary"]["failed"] == 0:
        lines.append("✅ **所有测试通过**：班级管理模块在所有用户角色下均按预期工作。")
        lines.append("")
        lines.append("- admin 角色可以完整访问 /admin/school/classes（班级 CRUD）")
        lines.append("- teacher（兼 grade_head）角色可以访问 /management/grade/classes（班级 CRUD）和 /teacher/classes/my（查看）")
        lines.append("- student / parent 角色被正确重定向到各自仪表盘（带 `reason=forbidden` 参数），权限隔离正常")
    else:
        lines.append(f"❌ **{results['summary']['failed']} 项测试失败**：请查看上方失败项详情并修复。")
    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"{MODULE_NAME}_{VERSION}.md"
    with open(output_path, "w", encoding="utf-8") as f:
        f.write(report)
    print(f"\n📄 报告已写入: {output_path}")

    json_path = WEBTEST_DIR / f"{MODULE_NAME}_{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()
