"""
测试：引擎模块
测试规则引擎和混合引擎
"""
import pytest
from unittest.mock import AsyncMock, MagicMock, patch
import json

from playwright_ez.ai.types import AIConfig, LLMConfig, LLMProvider, SelectorSuggestion
from playwright_ez.ai.engine.rule_engine import RuleEngine
from playwright_ez.ai.engine.hybrid_engine import HybridEngine


class MockPage:
    """模拟 Page 对象"""

    def __init__(self):
        self.url = "https://example.com"
        self._locator = MagicMock()

    def locator(self, selector: str):
        return self._locator


class TestRuleEngine:
    """测试规则引擎"""

    @pytest.fixture
    def mock_page(self):
        """创建模拟页面"""
        page = MockPage()
        # 配置 locator 的 count 方法返回 1
        page._locator.count = AsyncMock(return_value=1)
        return page

    @pytest.fixture
    def rule_engine(self, mock_page):
        """创建规则引擎实例"""
        return RuleEngine(mock_page)

    @pytest.mark.asyncio
    async def test_suggest_selectors_for_button(self, rule_engine, mock_page):
        """测试按钮选择器建议"""
        suggestions = await rule_engine.suggest_selectors("登录按钮", "button")

        assert len(suggestions) > 0
        assert all(isinstance(s, SelectorSuggestion) for s in suggestions)
        # 检查是否有 button 相关的选择器
        selectors = [s.selector for s in suggestions]
        assert any("button" in s for s in selectors)

    @pytest.mark.asyncio
    async def test_suggest_selectors_for_input(self, rule_engine, mock_page):
        """测试输入框选择器建议"""
        suggestions = await rule_engine.suggest_selectors("用户名", "input")

        assert len(suggestions) > 0
        # 检查是否有 input 相关的选择器
        selectors = [s.selector for s in suggestions]
        assert any("input" in s or "placeholder" in s for s in selectors)

    @pytest.mark.asyncio
    async def test_suggest_selectors_for_link(self, rule_engine, mock_page):
        """测试链接选择器建议"""
        suggestions = await rule_engine.suggest_selectors("关于我们", "link")

        assert len(suggestions) > 0
        selectors = [s.selector for s in suggestions]
        assert any("a:" in s or "a[" in s for s in selectors)

    @pytest.mark.asyncio
    async def test_suggest_selectors_generic(self, rule_engine, mock_page):
        """测试通用选择器建议"""
        suggestions = await rule_engine.suggest_selectors("测试文本")

        assert len(suggestions) > 0
        # 应该有文本相关的选择器
        selectors = [s.selector for s in suggestions]
        assert any("text" in s.lower() for s in selectors)

    @pytest.mark.asyncio
    async def test_suggest_selectors_confidence_ordering(self, rule_engine, mock_page):
        """测试选择器按置信度排序"""
        # 配置多个元素匹配
        mock_page._locator.count = AsyncMock(return_value=1)

        suggestions = await rule_engine.suggest_selectors("按钮")

        # 检查是否按置信度降序排列
        if len(suggestions) > 1:
            for i in range(len(suggestions) - 1):
                assert suggestions[i].confidence >= suggestions[i + 1].confidence

    @pytest.mark.asyncio
    async def test_suggest_selectors_max_suggestions(self, mock_page):
        """测试最大建议数量限制"""
        config = AIConfig(max_suggestions=3)
        engine = RuleEngine(mock_page, config)

        suggestions = await engine.suggest_selectors("按钮")
        assert len(suggestions) <= 3

    @pytest.mark.asyncio
    async def test_diagnose_selector_not_found(self, rule_engine):
        """测试选择器未找到错误诊断"""
        error = Exception("Element not found: #missing")

        diagnosis = await rule_engine.diagnose_error(error)

        assert diagnosis.error_type == "selector_not_found"
        assert len(diagnosis.likely_causes) > 0
        assert len(diagnosis.suggested_solutions) > 0

    @pytest.mark.asyncio
    async def test_diagnose_timeout_error(self, rule_engine):
        """测试超时错误诊断"""
        error = Exception("Timeout waiting for element")

        diagnosis = await rule_engine.diagnose_error(error)

        assert diagnosis.error_type == "timeout"
        assert len(diagnosis.likely_causes) > 0
        assert len(diagnosis.suggested_solutions) > 0

    @pytest.mark.asyncio
    async def test_diagnose_navigation_error(self, rule_engine):
        """测试导航错误诊断"""
        error = Exception("Navigation failed to https://example.com")

        diagnosis = await rule_engine.diagnose_error(error)

        assert diagnosis.error_type == "navigation"

    @pytest.mark.asyncio
    async def test_diagnose_unknown_error(self, rule_engine):
        """测试未知错误诊断"""
        error = Exception("Something went wrong")

        diagnosis = await rule_engine.diagnose_error(error)

        assert diagnosis.error_type == "unknown"


class TestHybridEngine:
    """测试混合引擎"""

    @pytest.fixture
    def mock_page(self):
        """创建模拟页面"""
        page = MockPage()
        page._locator.count = AsyncMock(return_value=1)
        return page

    @pytest.fixture
    def rule_config(self):
        """纯规则引擎配置"""
        return AIConfig(
            llm=LLMConfig(provider=LLMProvider.RULE)
        )

    @pytest.fixture
    def custom_llm_config(self):
        """自定义 LLM 配置"""
        return AIConfig(
            llm=LLMConfig(
                provider=LLMProvider.CUSTOM,
                custom_call_fn=lambda x: json.dumps([
                    {"selector": "button.submit", "confidence": 0.9, "strategy": "class"}
                ])
            )
        )

    @pytest.mark.asyncio
    async def test_rule_mode(self, mock_page, rule_config):
        """测试纯规则模式"""
        engine = HybridEngine(mock_page, rule_config)

        suggestions = await engine.suggest_selectors("登录按钮")

        assert len(suggestions) > 0
        # 规则引擎生成的建议有特定格式
        assert all(s.strategy != "llm" for s in suggestions)

    @pytest.mark.asyncio
    async def test_custom_llm_mode(self, mock_page, custom_llm_config):
        """测试自定义 LLM 模式"""
        engine = HybridEngine(mock_page, custom_llm_config)

        suggestions = await engine.suggest_selectors("提交按钮")

        # 自定义 LLM 返回了有效的选择器
        assert len(suggestions) > 0
        assert suggestions[0].selector == "button.submit"

    @pytest.mark.asyncio
    async def test_fallback_to_rule(self, mock_page):
        """测试 LLM 失败时回退到规则引擎"""
        # 配置一个会失败的 LLM
        def failing_llm(prompt: str) -> str:
            raise Exception("LLM failed")

        config = AIConfig(
            llm=LLMConfig(
                provider=LLMProvider.CUSTOM,
                custom_call_fn=failing_llm
            ),
            fallback_to_rule=True
        )

        engine = HybridEngine(mock_page, config)

        suggestions = await engine.suggest_selectors("按钮")

        # 应该回退到规则引擎并返回结果
        assert len(suggestions) > 0

    @pytest.mark.asyncio
    async def test_fallback_disabled(self, mock_page):
        """测试禁用回退时 LLM 失败返回空"""
        def failing_llm(prompt: str) -> str:
            raise Exception("LLM failed")

        config = AIConfig(
            llm=LLMConfig(
                provider=LLMProvider.CUSTOM,
                custom_call_fn=failing_llm
            ),
            fallback_to_rule=False
        )

        engine = HybridEngine(mock_page, config)

        suggestions = await engine.suggest_selectors("按钮")

        # 禁用回退，LLM 失败时应返回空
        assert len(suggestions) == 0

    @pytest.mark.asyncio
    async def test_llm_json_parsing_with_markdown(self, mock_page):
        """测试 LLM 返回 markdown 格式的 JSON"""
        def llm_with_markdown(prompt: str) -> str:
            return """```json
[{"selector": "#submit-btn", "confidence": 0.95, "strategy": "id"}]
```"""

        config = AIConfig(
            llm=LLMConfig(
                provider=LLMProvider.CUSTOM,
                custom_call_fn=llm_with_markdown
            )
        )

        engine = HybridEngine(mock_page, config)

        suggestions = await engine.suggest_selectors("提交")

        assert len(suggestions) > 0
        assert suggestions[0].selector == "#submit-btn"

    @pytest.mark.asyncio
    async def test_llm_diagnose_error(self, mock_page, custom_llm_config):
        """测试 LLM 错误诊断"""
        engine = HybridEngine(mock_page, custom_llm_config)

        # 修改 LLM 返回诊断结果
        custom_llm_config.llm.custom_call_fn = lambda x: json.dumps({
            "error_type": "selector_issue",
            "likely_causes": ["元素不存在", "选择器错误"],
            "suggested_solutions": ["检查选择器", "添加等待"],
            "related_elements": []
        })

        error = Exception("Element not found")
        diagnosis = await engine.diagnose_error(error)

        assert diagnosis.error_type == "selector_issue"
        assert len(diagnosis.likely_causes) > 0

    @pytest.mark.asyncio
    async def test_use_llm_first_disabled(self, mock_page):
        """测试禁用 LLM 优先"""
        config = AIConfig(
            llm=LLMConfig(
                provider=LLMProvider.CUSTOM,
                custom_call_fn=lambda x: "[]"
            ),
            use_llm_first=False
        )

        engine = HybridEngine(mock_page, config)

        suggestions = await engine.suggest_selectors("按钮")

        # 禁用 LLM 优先，直接使用规则引擎
        assert len(suggestions) > 0


class TestAIConfig:
    """测试 AIConfig 配置类"""

    def test_default_values(self):
        """测试默认值"""
        config = AIConfig()

        assert config.enable_healing is True
        assert config.enable_suggestions is True
        assert config.max_suggestions == 5
        assert config.use_llm_first is True
        assert config.fallback_to_rule is True
        assert config.llm.provider == LLMProvider.RULE

    def test_custom_llm_config(self):
        """测试自定义 LLM 配置"""
        config = AIConfig(
            llm=LLMConfig(
                provider=LLMProvider.CLAUDE,
                claude_api_key="sk-test"
            )
        )

        assert config.llm.provider == LLMProvider.CLAUDE
        assert config.llm.claude_api_key == "sk-test"

    def test_hybrid_mode_config(self):
        """测试混合模式配置"""
        config = AIConfig(
            use_llm_first=True,
            fallback_to_rule=True,
            llm=LLMConfig(provider=LLMProvider.OPENAI)
        )

        assert config.use_llm_first is True
        assert config.fallback_to_rule is True
        assert config.llm.provider == LLMProvider.OPENAI


if __name__ == "__main__":
    pytest.main([__file__, "-v"])


class TestAIUnifiedAccess:
    """测试 AI 模块统一入口访问"""

    @pytest.mark.asyncio
    async def test_browser_ai_accessor(self):
        """测试 browser.ai 统一访问入口"""
        from playwright_ez import EasyBrowser

        async with EasyBrowser() as browser:
            # 验证 ai 属性存在且类型正确
            from playwright_ez.ai import AIHelper
            assert isinstance(browser.ai, AIHelper)

    @pytest.mark.asyncio
    async def test_browser_ai_suggest_selectors(self):
        """测试通过 browser.ai 进行选择器建议"""
        from playwright_ez import EasyBrowser

        async with EasyBrowser() as browser:
            await browser.goto("https://www.baidu.com")

            # 使用统一入口进行选择器建议
            suggestions = await browser.ai.suggest_selectors("百度一下", "button")

            assert len(suggestions) > 0
            assert all(s.selector for s in suggestions)
            assert all(s.confidence > 0 for s in suggestions)

    @pytest.mark.asyncio
    async def test_browser_ai_diagnose_error(self):
        """测试通过 browser.ai 进行错误诊断"""
        from playwright_ez import EasyBrowser

        async with EasyBrowser() as browser:
            await browser.goto("https://www.baidu.com")

            # 模拟一个错误
            error = Exception("Element not found: #missing-element")
            diagnosis = await browser.ai.diagnose_error(error)

            assert diagnosis.error_type == "selector_not_found"
            assert len(diagnosis.likely_causes) > 0
            assert len(diagnosis.suggested_solutions) > 0

    @pytest.mark.asyncio
    async def test_browser_ai_generate_code(self):
        """测试通过 browser.ai 生成测试代码"""
        from playwright_ez import EasyBrowser

        async with EasyBrowser() as browser:
            await browser.goto("https://www.baidu.com")

            code = await browser.ai.generate_test_code("搜索测试")

            assert code.code is not None
            assert "async def" in code.code
            assert code.language == "python"

    @pytest.mark.asyncio
    async def test_browser_ai_with_custom_config(self):
        """测试带自定义配置的 browser.ai"""
        from playwright_ez import EasyBrowser, AIConfig, LLMConfig, LLMProvider

        # 使用规则引擎配置
        ai_config = AIConfig(
            llm=LLMConfig(provider=LLMProvider.RULE),
            max_suggestions=3
        )

        async with EasyBrowser() as browser:
            # 重新创建 AIHelper 使用自定义配置
            from playwright_ez.ai import AIHelper
            custom_ai = AIHelper(browser.page, ai_config)

            suggestions = await custom_ai.suggest_selectors("按钮")
            assert len(suggestions) <= 3

    @pytest.mark.asyncio
    async def test_ai_accessor_lazy_loading(self):
        """测试 AI 访问器懒加载"""
        from playwright_ez import EasyBrowser

        async with EasyBrowser() as browser:
            # 多次访问应返回同一实例
            ai1 = browser.ai
            ai2 = browser.ai
            assert ai1 is ai2

    @pytest.mark.asyncio
    async def test_ai_accessor_reset_on_tab_switch(self):
        """测试切换标签页时 AI 实例重置"""
        from playwright_ez import EasyBrowser

        async with EasyBrowser() as browser:
            ai1 = browser.ai

            # 创建新标签页
            await browser.new_tab("test_tab")

            # AI 实例应该被重置
            ai2 = browser.ai
            # 注意：这里不一定是同一个实例，因为切换标签页会重置
