"""
可访问性检查器
基于 WCAG 标准的可访问性检测
"""
from dataclasses import dataclass, field
from typing import Optional, List, Literal
from playwright.async_api import Page, Locator
from loguru import logger


ImpactLevel = Literal["minor", "moderate", "serious", "critical"]


@dataclass
class AccessibilityNode:
    """可访问性节点"""
    html: str
    target: List[str]
    failure_summary: str = ""


@dataclass
class AccessibilityViolation:
    """可访问性违规"""
    id: str
    impact: ImpactLevel
    description: str
    help: str
    help_url: str
    nodes: List[AccessibilityNode] = field(default_factory=list)


@dataclass
class AccessibilityPass:
    """可访问性通过项"""
    id: str
    description: str
    node_count: int = 0


@dataclass
class AccessibilityCheckResult:
    """可访问性检查结果"""
    passes: bool
    violations: List[AccessibilityViolation] = field(default_factory=list)
    incomplete: List[AccessibilityViolation] = field(default_factory=list)
    passes_detail: List[AccessibilityPass] = field(default_factory=list)


@dataclass
class AccessibilityCheckConfig:
    """可访问性检查配置"""
    impact_threshold: ImpactLevel = "minor"
    rules: Optional[List[str]] = None
    exclude_rules: Optional[List[str]] = None


class AccessibilityChecker:
    """
    可访问性检查器
    基于 WCAG 标准进行可访问性检测
    """

    SEVERITY_ORDER = ["minor", "moderate", "serious", "critical"]

    def __init__(
        self,
        page: Page,
        config: Optional[AccessibilityCheckConfig] = None
    ):
        self.page = page
        self.config = config or AccessibilityCheckConfig()

    async def check(self, selector: Optional[str] = None) -> AccessibilityCheckResult:
        """
        执行可访问性检查

        Args:
            selector: 要检查的元素选择器，默认检查整个页面

        Returns:
            AccessibilityCheckResult: 检查结果
        """
        target = self.page.locator(selector) if selector else self.page.locator("body")

        # 执行各项检查
        results = await asyncio.gather(
            self._check_images(target),
            self._check_labels(target),
            self._check_headings(target),
            self._check_links(target),
            self._check_forms(target),
            self._check_aria(target),
            self._check_color_contrast(target),
            self._check_keyboard(target),
        )

        violations: List[AccessibilityViolation] = []
        passes: List[AccessibilityPass] = []
        incomplete: List[AccessibilityViolation] = []

        for result in results:
            violations.extend(result["violations"])
            passes.extend(result["passes"])
            incomplete.extend(result["incomplete"])

        # 过滤规则
        if self.config.rules:
            rule_set = set(self.config.rules)
            violations = [v for v in violations if v.id in rule_set]
            passes = [p for p in passes if p.id in rule_set]

        if self.config.exclude_rules:
            exclude_set = set(self.config.exclude_rules)
            violations = [v for v in violations if v.id not in exclude_set]
            passes = [p for p in passes if p.id not in exclude_set]

        # 按严重程度过滤
        if self.config.impact_threshold:
            threshold_index = self.SEVERITY_ORDER.index(self.config.impact_threshold)
            violations = [
                v for v in violations
                if self.SEVERITY_ORDER.index(v.impact) >= threshold_index
            ]

        logger.debug(f"可访问性检查完成: violations={len(violations)}, passes={len(passes)}")

        return AccessibilityCheckResult(
            passes=len(violations) == 0,
            violations=violations,
            incomplete=incomplete,
            passes_detail=passes,
        )

    async def _check_images(self, target: Locator) -> dict:
        """检查图片"""
        violations: List[AccessibilityViolation] = []
        passes: List[AccessibilityPass] = []

        images = target.locator("img")
        count = await images.count()

        pass_count = 0
        for i in range(count):
            img = images.nth(i)
            alt = await img.get_attribute("alt")
            aria_label = await img.get_attribute("aria-label")
            aria_hidden = await img.get_attribute("aria-hidden")

            if alt is not None or aria_label or aria_hidden == "true":
                pass_count += 1
            else:
                html = await img.evaluate("el => el.outerHTML")
                violations.append(AccessibilityViolation(
                    id="image-alt",
                    impact="serious",
                    description="图片缺少替代文本",
                    help="为图片添加 alt 属性描述图片内容",
                    help_url="https://dequeuniversity.com/rules/axe/4.4/image-alt",
                    nodes=[AccessibilityNode(
                        html=html,
                        target=[f"img:nth-of-type({i + 1})"],
                        failure_summary="图片缺少 alt 属性",
                    )],
                ))

        if pass_count > 0:
            passes.append(AccessibilityPass(
                id="image-alt",
                description="图片有替代文本",
                node_count=pass_count,
            ))

        return {"violations": violations, "passes": passes, "incomplete": []}

    async def _check_labels(self, target: Locator) -> dict:
        """检查标签"""
        violations: List[AccessibilityViolation] = []
        passes: List[AccessibilityPass] = []

        inputs = target.locator('input:not([type="hidden"]):not([type="submit"]):not([type="button"])')
        count = await inputs.count()

        pass_count = 0
        for i in range(count):
            input_el = inputs.nth(i)
            input_id = await input_el.get_attribute("id")
            aria_label = await input_el.get_attribute("aria-label")
            aria_labelledby = await input_el.get_attribute("aria-labelledby")

            has_label = False

            if aria_label or aria_labelledby:
                has_label = True
            elif input_id:
                label = target.locator(f'label[for="{input_id}"]')
                label_count = await label.count()
                has_label = label_count > 0

            if has_label:
                pass_count += 1
            else:
                html = await input_el.evaluate("el => el.outerHTML")
                violations.append(AccessibilityViolation(
                    id="label",
                    impact="serious",
                    description="表单元素缺少关联标签",
                    help="为表单元素添加 label 或 aria-label",
                    help_url="https://dequeuniversity.com/rules/axe/4.4/label",
                    nodes=[AccessibilityNode(
                        html=html,
                        target=[f"input:nth-of-type({i + 1})"],
                        failure_summary="表单元素没有关联的标签",
                    )],
                ))

        if pass_count > 0:
            passes.append(AccessibilityPass(
                id="label",
                description="表单元素有标签",
                node_count=pass_count,
            ))

        return {"violations": violations, "passes": passes, "incomplete": []}

    async def _check_headings(self, target: Locator) -> dict:
        """检查标题层级"""
        violations: List[AccessibilityViolation] = []
        passes: List[AccessibilityPass] = []

        headings = target.locator("h1, h2, h3, h4, h5, h6")
        count = await headings.count()

        prev_level = 0
        pass_count = 0

        for i in range(count):
            heading = headings.nth(i)
            tag_name = await heading.evaluate("el => el.tagName.toLowerCase()")
            level = int(tag_name[1])

            if prev_level > 0 and level > prev_level + 1:
                html = await heading.evaluate("el => el.outerHTML")
                violations.append(AccessibilityViolation(
                    id="heading-order",
                    impact="moderate",
                    description="标题层级跳跃",
                    help="确保标题层级连续",
                    help_url="https://dequeuniversity.com/rules/axe/4.4/heading-order",
                    nodes=[AccessibilityNode(
                        html=html,
                        target=[f"{tag_name}:nth-of-type({i + 1})"],
                        failure_summary=f"标题从 h{prev_level} 跳到 h{level}",
                    )],
                ))
            else:
                pass_count += 1

            prev_level = level

        if pass_count > 0:
            passes.append(AccessibilityPass(
                id="heading-order",
                description="标题层级正确",
                node_count=pass_count,
            ))

        # 检查页面是否有 h1
        h1_count = await target.locator("h1").count()
        if h1_count == 0:
            violations.append(AccessibilityViolation(
                id="page-has-heading-one",
                impact="moderate",
                description="页面缺少一级标题",
                help="为页面添加唯一的 h1 标题",
                help_url="https://dequeuniversity.com/rules/axe/4.4/page-has-heading-one",
                nodes=[],
            ))
        elif h1_count >= 1:
            passes.append(AccessibilityPass(
                id="page-has-heading-one",
                description="页面有一级标题",
                node_count=h1_count,
            ))

        return {"violations": violations, "passes": passes, "incomplete": []}

    async def _check_links(self, target: Locator) -> dict:
        """检查链接"""
        violations: List[AccessibilityViolation] = []
        passes: List[AccessibilityPass] = []

        links = target.locator("a[href]")
        count = await links.count()

        pass_count = 0
        for i in range(count):
            link = links.nth(i)
            text = await link.text_content()
            aria_label = await link.get_attribute("aria-label")
            title = await link.get_attribute("title")

            if (text and text.strip()) or aria_label or title:
                pass_count += 1
            else:
                html = await link.evaluate("el => el.outerHTML")
                violations.append(AccessibilityViolation(
                    id="link-name",
                    impact="serious",
                    description="链接没有可识别的文本",
                    help="为链接添加可见文本或 aria-label",
                    help_url="https://dequeuniversity.com/rules/axe/4.4/link-name",
                    nodes=[AccessibilityNode(
                        html=html,
                        target=[f"a:nth-of-type({i + 1})"],
                        failure_summary="链接缺少可识别文本",
                    )],
                ))

        if pass_count > 0:
            passes.append(AccessibilityPass(
                id="link-name",
                description="链接有可识别文本",
                node_count=pass_count,
            ))

        return {"violations": violations, "passes": passes, "incomplete": []}

    async def _check_forms(self, target: Locator) -> dict:
        """检查表单"""
        violations: List[AccessibilityViolation] = []
        passes: List[AccessibilityPass] = []

        forms = target.locator("form")
        form_count = await forms.count()

        pass_count = 0
        for i in range(form_count):
            form = forms.nth(i)
            submit_button = form.locator('button[type="submit"], input[type="submit"]')
            has_submit = await submit_button.count() > 0

            if has_submit:
                pass_count += 1
            else:
                html = await form.evaluate("el => el.outerHTML.substring(0, 200)")
                violations.append(AccessibilityViolation(
                    id="form-submit",
                    impact="moderate",
                    description="表单缺少提交按钮",
                    help="为表单添加提交按钮",
                    help_url="https://dequeuniversity.com/rules/axe/4.4/label",
                    nodes=[AccessibilityNode(
                        html=html,
                        target=[f"form:nth-of-type({i + 1})"],
                        failure_summary="表单没有提交按钮",
                    )],
                ))

        if pass_count > 0:
            passes.append(AccessibilityPass(
                id="form-submit",
                description="表单有提交按钮",
                node_count=pass_count,
            ))

        return {"violations": violations, "passes": passes, "incomplete": []}

    async def _check_aria(self, target: Locator) -> dict:
        """检查 ARIA 属性"""
        violations: List[AccessibilityViolation] = []
        passes: List[AccessibilityPass] = []

        hidden_elements = target.locator('[aria-hidden="true"]')
        hidden_count = await hidden_elements.count()

        for i in range(hidden_count):
            element = hidden_elements.nth(i)
            focusable = element.locator("a, button, input, select, textarea, [tabindex]")
            focusable_count = await focusable.count()

            if focusable_count > 0:
                html = await element.evaluate("el => el.outerHTML.substring(0, 200)")
                violations.append(AccessibilityViolation(
                    id="aria-hidden-focus",
                    impact="serious",
                    description="隐藏元素包含可聚焦元素",
                    help="确保 aria-hidden 元素内的交互元素不可聚焦",
                    help_url="https://dequeuniversity.com/rules/axe/4.4/aria-hidden-focus",
                    nodes=[AccessibilityNode(
                        html=html,
                        target=[f'[aria-hidden="true"]:nth-of-type({i + 1})'],
                        failure_summary="隐藏元素包含可聚焦的子元素",
                    )],
                ))

        passes.append(AccessibilityPass(
            id="aria-valid-attr",
            description="ARIA 属性有效",
            node_count=hidden_count,
        ))

        return {"violations": violations, "passes": passes, "incomplete": []}

    async def _check_color_contrast(self, target: Locator) -> dict:
        """检查颜色对比度（简化版）"""
        violations: List[AccessibilityViolation] = []
        passes: List[AccessibilityPass] = []

        styled_elements = target.locator('[style*="color"], [style*="background"]')
        count = await styled_elements.count()

        # 实际项目中应该使用专门的对比度计算库
        passes.append(AccessibilityPass(
            id="color-contrast",
            description="颜色对比度检查（需手动验证）",
            node_count=count,
        ))

        return {"violations": violations, "passes": passes, "incomplete": []}

    async def _check_keyboard(self, target: Locator) -> dict:
        """检查键盘可访问性"""
        violations: List[AccessibilityViolation] = []
        passes: List[AccessibilityPass] = []

        click_elements = target.locator("[onclick]")
        count = await click_elements.count()

        pass_count = 0
        for i in range(count):
            element = click_elements.nth(i)
            tabindex = await element.get_attribute("tabindex")
            tag_name = await element.evaluate("el => el.tagName.toLowerCase()")

            is_focusable = tag_name in ["a", "button", "input", "select", "textarea"] or tabindex is not None

            if is_focusable:
                pass_count += 1
            else:
                html = await element.evaluate("el => el.outerHTML.substring(0, 200)")
                violations.append(AccessibilityViolation(
                    id="keyboard",
                    impact="serious",
                    description="可点击元素无法通过键盘访问",
                    help="为元素添加 tabindex 或使用 button/a 元素",
                    help_url="https://dequeuniversity.com/rules/axe/4.4/keyboard",
                    nodes=[AccessibilityNode(
                        html=html,
                        target=[f"[onclick]:nth-of-type({i + 1})"],
                        failure_summary="元素可通过鼠标点击但无法通过键盘聚焦",
                    )],
                ))

        if pass_count > 0:
            passes.append(AccessibilityPass(
                id="keyboard",
                description="可点击元素可通过键盘访问",
                node_count=pass_count,
            ))

        return {"violations": violations, "passes": passes, "incomplete": []}

    def generate_report(self, result: AccessibilityCheckResult) -> str:
        """
        生成报告

        Args:
            result: 检查结果

        Returns:
            str: 报告文本
        """
        lines = [
            "\n=== 可访问性检查报告 ===\n",
            f"状态: {'✅ 通过' if result.passes else '❌ 未通过'}",
            f"违规项: {len(result.violations)}",
            f"不完整项: {len(result.incomplete)}",
            f"通过项: {len(result.passes_detail)}\n",
        ]

        if result.violations:
            lines.append("违规详情:")
            for v in result.violations:
                lines.extend([
                    f"\n[{v.impact.upper()}] {v.id}",
                    f"  {v.description}",
                    f"  建议: {v.help}",
                    f"  文档: {v.help_url}",
                ])
                if v.nodes:
                    lines.append(f"  影响元素: {len(v.nodes)} 个")

        return "\n".join(lines)


import asyncio


def create_accessibility_checker(
    page: Page,
    config: Optional[AccessibilityCheckConfig] = None
) -> AccessibilityChecker:
    """创建可访问性检查器实例"""
    return AccessibilityChecker(page, config)
