#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
模块名称：selector_extractor.py
模块描述：选择器提取模块，从 Playwright 元素对象中提取多种候选选择器

作者：czj
创建日期：2026-08-05
最后修改：2026-08-05
"""

import logging
from typing import List, Dict, Any, Optional

logger = logging.getLogger(__name__)


# ==================== 选择器优先级 ====================
# 优先级越高，置信度越高，执行时优先使用

SELECTOR_PRIORITY = {
    'data_testid': 1,    # data-testid 属性（最稳定）
    'id': 2,              # ID 属性（唯一）
    'name': 3,            # name 属性（表单元素）
    'placeholder': 4,     # placeholder 属性（输入框）
    'aria_label': 5,      # aria-label 属性（无障碍）
    'text': 6,            # 文本内容（按钮、链接）
    'class': 7,           # class 组合选择器
    'xpath': 8,           # XPath（通用但脆弱）
}


# ==================== 选择器提取 ====================

def extract_selectors(element) -> Dict[str, Any]:
    """
    从 Playwright 元素对象中提取多种候选选择器

    按优先级提取：
    1. data-testid / data-test-id
    2. ID
    3. name
    4. placeholder
    5. aria-label
    6. 文本内容（按钮、链接）
    7. class 组合选择器
    8. XPath

    Args:
        element: Playwright Locator 对象

    Returns:
        Dict: {
            'primary': str,  # 主选择器（最高优先级）
            'candidates': [  # 候选选择器列表
                {'type': str, 'value': str, 'confidence': float, 'priority': int}
            ]
        }

    Example:
        >>> extract_selectors(button_element)
        {
            'primary': 'button:has-text("登录")',
            'candidates': [
                {'type': 'css', 'value': 'button:has-text("登录")', 'confidence': 0.85, 'priority': 6},
                {'type': 'css', 'value': '.el-button--primary', 'confidence': 0.60, 'priority': 7}
            ]
        }
    """
    candidates = []

    try:
        # 提取元素属性
        tag = element.evaluate('el => el.tagName')
        el_type = element.get_attribute('type') or ''
        placeholder = element.get_attribute('placeholder') or ''
        text = element.inner_text().strip() if tag in ['BUTTON', 'A', 'LABEL'] else ''
        el_id = element.get_attribute('id') or ''
        name = element.get_attribute('name') or ''
        aria_label = element.get_attribute('aria-label') or ''
        data_testid = element.get_attribute('data-testid') or element.get_attribute('data-test-id') or ''
        class_name = element.get_attribute('class') or ''
        href = element.get_attribute('href') or ''
        role = element.get_attribute('role') or ''

        # 1. data-testid 选择器（最稳定）
        if data_testid:
            candidates.append({
                'type': 'css',
                'value': f'[data-testid="{data_testid}"]',
                'confidence': 0.95,
                'priority': SELECTOR_PRIORITY['data_testid']
            })

        # 2. ID 选择器
        if el_id:
            # ID 可能包含特殊字符，用 CSS 转义
            escaped_id = _css_escape_id(el_id)
            candidates.append({
                'type': 'css',
                'value': f'#{escaped_id}',
                'confidence': 0.90,
                'priority': SELECTOR_PRIORITY['id']
            })

        # 3. name 选择器（表单元素）
        if name and tag in ['INPUT', 'SELECT', 'TEXTAREA']:
            candidates.append({
                'type': 'css',
                'value': f'{tag.lower()}[name="{name}"]',
                'confidence': 0.85,
                'priority': SELECTOR_PRIORITY['name']
            })

        # 4. placeholder 选择器（输入框）
        if placeholder and tag == 'INPUT':
            candidates.append({
                'type': 'css',
                'value': f'input[placeholder*="{placeholder}"]',
                'confidence': 0.80,
                'priority': SELECTOR_PRIORITY['placeholder']
            })

        # 5. aria-label 选择器
        if aria_label:
            candidates.append({
                'type': 'css',
                'value': f'[{tag.lower()}][aria-label*="{aria_label}"]',
                'confidence': 0.75,
                'priority': SELECTOR_PRIORITY['aria_label']
            })

        # 6. 文本选择器（按钮、链接）
        if text and tag in ['BUTTON', 'A']:
            candidates.append({
                'type': 'css',
                'value': f'{tag.lower()}:has-text("{text}")',
                'confidence': 0.85,
                'priority': SELECTOR_PRIORITY['text']
            })

        # 7. role 选择器
        if role:
            candidates.append({
                'type': 'css',
                'value': f'[role="{role}"]',
                'confidence': 0.70,
                'priority': SELECTOR_PRIORITY['class']
            })

        # 8. href 选择器（链接）
        if href and tag == 'A':
            # 提取 href 的关键部分
            href_part = href.split('/')[-1] or href.split('?')[0].split('/')[-1]
            if href_part:
                candidates.append({
                    'type': 'css',
                    'value': f'a[href*="{href_part}"]',
                    'confidence': 0.65,
                    'priority': SELECTOR_PRIORITY['class']
                })

        # 9. class 组合选择器（选择最独特的 class）
        if class_name and len(class_name.split()) > 0:
            classes = class_name.split()
            # 优先选择看起来唯一的 class（包含 ID、hash、unique 等词）
            unique_classes = [c for c in classes if any(kw in c.lower() for kw in ['id', 'unique', 'hash', 'uuid'])]
            if unique_classes:
                candidates.append({
                    'type': 'css',
                    'value': f'.{unique_classes[0]}',
                    'confidence': 0.60,
                    'priority': SELECTOR_PRIORITY['class']
                })
            # 其次选择组件特定的 class（如 el-button--primary）
            elif any('el-' in c or 'ant-' in c or 'v-' in c for c in classes):
                component_classes = [c for c in classes if 'el-' in c or 'ant-' in c or 'v-' in c]
                if component_classes:
                    candidates.append({
                        'type': 'css',
                        'value': f'{tag.lower()}.{component_classes[0]}',
                        'confidence': 0.55,
                        'priority': SELECTOR_PRIORITY['class']
                    })

        # 10. type 选择器（输入框）
        if el_type and tag == 'INPUT':
            candidates.append({
                'type': 'css',
                'value': f'input[type="{el_type}"]',
                'confidence': 0.50,
                'priority': SELECTOR_PRIORITY['xpath']
            })

        # 11. XPath 回退（通用但脆弱）
        xpath = _generate_xpath(element)
        if xpath:
            candidates.append({
                'type': 'xpath',
                'value': xpath,
                'confidence': 0.40,
                'priority': SELECTOR_PRIORITY['xpath']
            })

    except Exception as e:
        logger.warning(f"提取选择器失败: {e}")

    # 按优先级排序
    candidates.sort(key=lambda x: x['priority'])

    # 选择主选择器（最高优先级）
    primary = candidates[0]['value'] if candidates else None

    return {
        'primary': primary,
        'candidates': candidates
    }


def extract_element_info(element) -> Dict[str, Any]:
    """
    提取元素的详细信息

    Args:
        element: Playwright Locator 对象

    Returns:
        Dict: 元素信息字典
    """
    try:
        tag = element.evaluate('el => el.tagName')
        return {
            'tag': tag,
            'type': element.get_attribute('type') or '',
            'placeholder': element.get_attribute('placeholder') or '',
            'text': element.inner_text().strip() if tag in ['BUTTON', 'A', 'LABEL'] else '',
            'id': element.get_attribute('id') or '',
            'name': element.get_attribute('name') or '',
            'className': element.get_attribute('class') or '',
            'ariaLabel': element.get_attribute('aria-label') or '',
            'href': element.get_attribute('href') or '',
            'role': element.get_attribute('role') or '',
        }
    except Exception as e:
        logger.warning(f"提取元素信息失败: {e}")
        return {}


# ==================== 辅助函数 ====================

def _css_escape_id(id_value: str) -> str:
    """
    CSS ID 转义（处理特殊字符）

    Args:
        id_value (str): 原始 ID 值

    Returns:
        str: 转义后的 ID
    """
    # 简单实现：只保留字母、数字、连字符、下划线
    import re
    escaped = re.sub(r'[^a-zA-Z0-9_-]', '-', id_value)
    return escaped


def _generate_xpath(element) -> Optional[str]:
    """
    为元素生成 XPath（回退策略）

    Args:
        element: Playwright Locator 对象

    Returns:
        Optional[str]: XPath 字符串
    """
    try:
        # 使用 JavaScript 生成 XPath
        xpath = element.evaluate('''el => {
            if (el.id) return '//*[@id="' + el.id + '"]';

            // 简单路径生成
            const parts = [];
            let current = el;
            while (current && current.nodeType === Node.ELEMENT_NODE) {
                let index = 0;
                let sibling = current.previousSibling;
                while (sibling) {
                    if (sibling.nodeType === Node.ELEMENT_NODE && sibling.tagName === current.tagName) {
                        index++;
                    }
                    sibling = sibling.previousSibling;
                }
                const tagName = current.tagName.toLowerCase();
                const indexStr = index > 0 ? `[${index + 1}]` : '';
                parts.unshift(tagName + indexStr);
                current = current.parentNode;
            }
            return '/' + parts.join('/');
        }''')
        return xpath
    except Exception as e:
        logger.debug(f"生成 XPath 失败: {e}")
        return None


def build_step_with_selectors(
    order: int,
    name: str,
    action: str,
    params: Dict[str, Any],
    selectors: Dict[str, Any]
) -> Dict[str, Any]:
    """
    构建包含选择器的步骤定义

    Args:
        order (int): 步骤顺序
        name (str): 步骤名称
        action (str): 动作类型
        params (dict): 动作参数
        selectors (dict): 选择器信息（extract_selectors 的返回值）

    Returns:
        Dict: 完整的步骤定义
    """
    # 将主选择器添加到 params
    params_with_selector = params.copy()
    if selectors.get('primary'):
        params_with_selector['selector'] = selectors['primary']

    # 构建候选选择器列表（用于回退）
    candidate_selectors = []
    for cand in selectors.get('candidates', []):
        candidate_selectors.append(cand['value'])

    return {
        'order': order,
        'name': name,
        'action': action,
        'params': params_with_selector,
        'expected': '',
        'actual': '',
        'selectors': candidate_selectors,
        'locator_type': 'css',
        'locator_value': selectors.get('primary', '')
    }