"""GEO Skill 工具绑定与端到端验证 — U4 集成测试

验证：
- SkillConfig with tools 字段加载正确
- ConfigDrivenAgent 从 ToolRegistry 注册声明的工具
- citation_detector 绑定 search + crawl 工具
- competitor_analyzer 绑定 search + crawl 工具
- geo_optimizer 绑定 schema_generate 工具
- schema_advisor 绑定 extract + generate 工具
- Tool 不在 ToolRegistry 中时优雅降级（log warning, skip）
- GEO Pipeline 配置加载正确
"""

import os
from unittest.mock import AsyncMock

import pytest
import yaml

from agentkit.core.config_driven import AgentConfig, ConfigDrivenAgent
from agentkit.skills.base import Skill, SkillConfig
from agentkit.skills.loader import SkillLoader
from agentkit.skills.registry import SkillRegistry
from agentkit.tools.baidu_search import BaiduSearchTool
from agentkit.tools.base import Tool
from agentkit.tools.function_tool import FunctionTool
from agentkit.tools.registry import ToolRegistry
from agentkit.tools.schema_tools import SchemaExtractTool, SchemaGenerateTool
from agentkit.tools.web_crawl import WebCrawlTool


# ── Fixtures ────────────────────────────────────────────────

CONFIGS_DIR = os.path.join(
    os.path.dirname(__file__), "..", "..", "configs"
)
SKILLS_DIR = os.path.join(CONFIGS_DIR, "skills")
PIPELINES_DIR = os.path.join(CONFIGS_DIR, "pipelines")


@pytest.fixture
def tool_registry_with_infra_tools():
    """创建包含基础设施工具的 ToolRegistry"""
    registry = ToolRegistry()
    registry.register(BaiduSearchTool())
    registry.register(WebCrawlTool())
    registry.register(SchemaExtractTool())
    registry.register(SchemaGenerateTool())
    return registry


@pytest.fixture
def tool_registry_empty():
    """创建空的 ToolRegistry（用于测试工具不可用时的降级）"""
    return ToolRegistry()


# ── Test: SkillConfig tools 字段加载 ────────────────────────


class TestSkillConfigToolsField:
    """验证 SkillConfig 的 tools 字段正确加载"""

    def test_citation_detector_tools_loaded(self):
        """citation_detector YAML 加载后 tools 包含 baidu_search + web_crawl"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "citation_detector.yaml")
        )
        assert "baidu_search" in config.tools
        assert "web_crawl" in config.tools
        # 原有业务工具也保留
        assert "execute_single_platform" in config.tools
        assert "get_or_create_task" in config.tools

    def test_competitor_analyzer_tools_loaded(self):
        """competitor_analyzer YAML 加载后 tools 包含 baidu_search + web_crawl"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "competitor_analyzer.yaml")
        )
        assert "baidu_search" in config.tools
        assert "web_crawl" in config.tools
        assert "competitor_analyze" in config.tools

    def test_geo_optimizer_tools_loaded(self):
        """geo_optimizer YAML 加载后 tools 包含 schema_generate"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "geo_optimizer.yaml")
        )
        assert "schema_generate" in config.tools

    def test_schema_advisor_tools_loaded(self):
        """schema_advisor YAML 加载后 tools 包含 schema_extract + schema_generate"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "schema_advisor.yaml")
        )
        assert "schema_extract" in config.tools
        assert "schema_generate" in config.tools
        assert "fill_schema_with_llm" in config.tools

    def test_monitor_tools_loaded(self):
        """monitor YAML 加载后 tools 包含 baidu_search"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "monitor.yaml")
        )
        assert "baidu_search" in config.tools
        assert "monitor_check_and_compare" in config.tools

    def test_trend_agent_tools_loaded(self):
        """trend_agent YAML 加载后 tools 包含 baidu_search + web_crawl"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "trend_agent.yaml")
        )
        assert "baidu_search" in config.tools
        assert "web_crawl" in config.tools

    def test_content_generator_tools_loaded(self):
        """content_generator YAML 加载后 tools 包含 baidu_search"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "content_generator.yaml")
        )
        assert "baidu_search" in config.tools
        assert "retrieve_knowledge" in config.tools

    def test_deai_agent_tools_loaded(self):
        """deai_agent YAML 加载后 tools 包含 detect_ai_patterns"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "deai_agent.yaml")
        )
        assert "detect_ai_patterns" in config.tools

    def test_all_skills_load_without_error(self):
        """所有 GEO Skill YAML 都能成功加载"""
        yaml_files = [
            "citation_detector.yaml",
            "competitor_analyzer.yaml",
            "geo_optimizer.yaml",
            "monitor.yaml",
            "schema_advisor.yaml",
            "trend_agent.yaml",
            "content_generator.yaml",
            "deai_agent.yaml",
        ]
        for filename in yaml_files:
            config = SkillConfig.from_yaml(
                os.path.join(SKILLS_DIR, filename)
            )
            assert config.name, f"{filename} should have a name"
            assert config.tools is not None, f"{filename} should have tools field"


# ── Test: ConfigDrivenAgent 工具绑定 ────────────────────────


class TestConfigDrivenAgentToolBinding:
    """验证 ConfigDrivenAgent 从 ToolRegistry 注册声明的工具"""

    def test_citation_detector_binds_search_and_crawl(
        self, tool_registry_with_infra_tools
    ):
        """citation_detector 绑定 baidu_search + web_crawl 工具"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "citation_detector.yaml")
        )
        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry_with_infra_tools,
        )
        tool_names = [t.name for t in agent.get_tools()]
        assert "baidu_search" in tool_names
        assert "web_crawl" in tool_names

    def test_competitor_analyzer_binds_search_and_crawl(
        self, tool_registry_with_infra_tools
    ):
        """competitor_analyzer 绑定 baidu_search + web_crawl 工具"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "competitor_analyzer.yaml")
        )
        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry_with_infra_tools,
        )
        tool_names = [t.name for t in agent.get_tools()]
        assert "baidu_search" in tool_names
        assert "web_crawl" in tool_names

    def test_geo_optimizer_binds_schema_generate(
        self, tool_registry_with_infra_tools
    ):
        """geo_optimizer 绑定 schema_generate 工具"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "geo_optimizer.yaml")
        )
        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry_with_infra_tools,
        )
        tool_names = [t.name for t in agent.get_tools()]
        assert "schema_generate" in tool_names

    def test_schema_advisor_binds_extract_and_generate(
        self, tool_registry_with_infra_tools
    ):
        """schema_advisor 绑定 schema_extract + schema_generate 工具"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "schema_advisor.yaml")
        )
        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry_with_infra_tools,
        )
        tool_names = [t.name for t in agent.get_tools()]
        assert "schema_extract" in tool_names
        assert "schema_generate" in tool_names

    def test_monitor_binds_search(
        self, tool_registry_with_infra_tools
    ):
        """monitor 绑定 baidu_search 工具"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "monitor.yaml")
        )
        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry_with_infra_tools,
        )
        tool_names = [t.name for t in agent.get_tools()]
        assert "baidu_search" in tool_names

    def test_trend_agent_binds_search_and_crawl(
        self, tool_registry_with_infra_tools
    ):
        """trend_agent 绑定 baidu_search + web_crawl 工具"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "trend_agent.yaml")
        )
        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry_with_infra_tools,
        )
        tool_names = [t.name for t in agent.get_tools()]
        assert "baidu_search" in tool_names
        assert "web_crawl" in tool_names


# ── Test: 工具不可用时优雅降级 ──────────────────────────────


class TestToolNotFoundGracefulDegradation:
    """验证 Tool 不在 ToolRegistry 中时优雅降级"""

    def test_missing_tool_does_not_crash(self, tool_registry_empty):
        """Tool 不在 ToolRegistry 中时 Agent 不会崩溃"""
        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "citation_detector.yaml")
        )
        # citation_detector 声明了 baidu_search, web_crawl, execute_single_platform, get_or_create_task
        # 这些工具都不在空 registry 中
        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry_empty,
        )
        # Agent 应该成功创建，只是没有绑定任何工具
        assert agent is not None
        assert len(agent.get_tools()) == 0

    def test_partial_tool_binding(self):
        """部分工具在 Registry 中时，只绑定可用的工具"""
        registry = ToolRegistry()
        registry.register(BaiduSearchTool())
        # 只注册了 baidu_search，没有 web_crawl

        config = SkillConfig.from_yaml(
            os.path.join(SKILLS_DIR, "citation_detector.yaml")
        )
        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=registry,
        )
        tool_names = [t.name for t in agent.get_tools()]
        # baidu_search 应该绑定成功
        assert "baidu_search" in tool_names
        # web_crawl 不在 registry 中，不应该绑定
        assert "web_crawl" not in tool_names


# ── Test: SkillLoader 批量加载 ──────────────────────────────


class TestSkillLoaderBatchLoad:
    """验证 SkillLoader 从目录批量加载 Skill 并绑定工具"""

    def test_load_all_geo_skills(self, tool_registry_with_infra_tools):
        """从 skills 目录加载所有 GEO Skill"""
        skill_registry = SkillRegistry()
        loader = SkillLoader(
            skill_registry=skill_registry,
            tool_registry=tool_registry_with_infra_tools,
        )
        skills = loader.load_from_directory(SKILLS_DIR)

        # 验证所有 Skill 都加载成功
        skill_names = [s.name for s in skills]
        assert "citation_detector" in skill_names
        assert "competitor_analyzer" in skill_names
        assert "geo_optimizer" in skill_names
        assert "monitor" in skill_names
        assert "schema_advisor" in skill_names
        assert "trend_agent" in skill_names
        assert "content_generator" in skill_names
        assert "deai_agent" in skill_names

    def test_citation_detector_skill_has_tools(
        self, tool_registry_with_infra_tools
    ):
        """citation_detector Skill 绑定了 search + crawl 工具"""
        skill_registry = SkillRegistry()
        loader = SkillLoader(
            skill_registry=skill_registry,
            tool_registry=tool_registry_with_infra_tools,
        )
        loader.load_from_directory(SKILLS_DIR)

        skill = skill_registry.get("citation_detector")
        tool_names = [t.name for t in skill.tools]
        assert "baidu_search" in tool_names
        assert "web_crawl" in tool_names

    def test_schema_advisor_skill_has_tools(
        self, tool_registry_with_infra_tools
    ):
        """schema_advisor Skill 绑定了 extract + generate 工具"""
        skill_registry = SkillRegistry()
        loader = SkillLoader(
            skill_registry=skill_registry,
            tool_registry=tool_registry_with_infra_tools,
        )
        loader.load_from_directory(SKILLS_DIR)

        skill = skill_registry.get("schema_advisor")
        tool_names = [t.name for t in skill.tools]
        assert "schema_extract" in tool_names
        assert "schema_generate" in tool_names

    def test_geo_optimizer_skill_has_schema_generate(
        self, tool_registry_with_infra_tools
    ):
        """geo_optimizer Skill 绑定了 schema_generate 工具"""
        skill_registry = SkillRegistry()
        loader = SkillLoader(
            skill_registry=skill_registry,
            tool_registry=tool_registry_with_infra_tools,
        )
        loader.load_from_directory(SKILLS_DIR)

        skill = skill_registry.get("geo_optimizer")
        tool_names = [t.name for t in skill.tools]
        assert "schema_generate" in tool_names


# ── Test: GEO Pipeline 配置加载 ──────────────────────────────


class TestGEOPipelineConfig:
    """验证 GEO Pipeline 配置加载正确"""

    def test_pipeline_config_loads(self):
        """geo_full_pipeline.yaml 能成功加载"""
        with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
            config = yaml.safe_load(f)
        assert config["name"] == "geo_full_pipeline"
        assert len(config["steps"]) > 0

    def test_pipeline_has_all_steps(self):
        """Pipeline 包含所有 GEO 步骤"""
        with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
            config = yaml.safe_load(f)

        step_names = [s["name"] for s in config["steps"]]
        # 核心步骤
        assert "detect" in step_names
        assert "analyze_competitor" in step_names
        assert "analyze_trend" in step_names
        assert "optimize" in step_names
        assert "schema" in step_names
        assert "monitor" in step_names
        # 新增步骤
        assert "generate_content" in step_names
        assert "deai" in step_names

    def test_pipeline_step_skills_match_yaml_names(self):
        """Pipeline 步骤的 skill 字段与 YAML 文件中的 name 一致"""
        with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
            config = yaml.safe_load(f)

        for step in config["steps"]:
            skill_name = step["skill"]
            yaml_path = os.path.join(SKILLS_DIR, f"{skill_name}.yaml")
            assert os.path.exists(yaml_path), (
                f"Pipeline step '{step['name']}' references skill "
                f"'{skill_name}' but {yaml_path} does not exist"
            )

    def test_pipeline_dependency_graph_is_valid(self):
        """Pipeline 依赖关系有效（无循环依赖）"""
        with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
            config = yaml.safe_load(f)

        step_map = {s["name"]: s.get("depends_on", []) for s in config["steps"]}

        # 拓扑排序检测循环依赖
        visited = set()
        in_stack = set()

        def dfs(name):
            if name in in_stack:
                return False  # 循环依赖
            if name in visited:
                return True
            in_stack.add(name)
            for dep in step_map.get(name, []):
                if not dfs(dep):
                    return False
            in_stack.discard(name)
            visited.add(name)
            return True

        for name in step_map:
            assert dfs(name), f"Circular dependency detected involving '{name}'"

    def test_pipeline_from_config_creates_pipeline(self):
        """GEOPipeline.from_config 能从 YAML 配置创建 Pipeline"""
        from agentkit.skills.geo_pipeline import GEOPipeline

        with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
            config = yaml.safe_load(f)

        pipeline = GEOPipeline.from_config(config)
        assert pipeline.name == "geo_full_pipeline"
        assert len(pipeline._steps) == len(config["steps"])

    def test_pipeline_execution_order_respects_dependencies(self):
        """Pipeline 执行顺序尊重依赖关系"""
        from agentkit.skills.geo_pipeline import GEOPipeline

        with open(os.path.join(PIPELINES_DIR, "geo_full_pipeline.yaml"), "r") as f:
            config = yaml.safe_load(f)

        pipeline = GEOPipeline.from_config(config)
        groups = pipeline._build_execution_groups()

        # 展平执行顺序
        executed = set()
        for group in groups:
            for step_name in group:
                step = pipeline._step_map[step_name]
                # 所有依赖必须已经执行
                for dep in step.depends_on:
                    assert dep in executed, (
                        f"Step '{step_name}' depends on '{dep}' "
                        f"but '{dep}' hasn't been executed yet"
                    )
                executed.add(step_name)

        # 所有步骤都应该被执行
        assert executed == set(pipeline._step_map.keys())


# ── Test: 基础设施工具实例化 ──────────────────────────────────


class TestInfrastructureToolsInstantiation:
    """验证基础设施工具能正确实例化"""

    def test_baidu_search_tool_instantiation(self):
        """BaiduSearchTool 能正确实例化"""
        tool = BaiduSearchTool()
        assert tool.name == "baidu_search"
        assert "search" in tool.tags

    def test_web_crawl_tool_instantiation(self):
        """WebCrawlTool 能正确实例化"""
        tool = WebCrawlTool()
        assert tool.name == "web_crawl"
        assert "crawl" in tool.tags

    def test_schema_extract_tool_instantiation(self):
        """SchemaExtractTool 能正确实例化"""
        tool = SchemaExtractTool()
        assert tool.name == "schema_extract"
        assert "extraction" in tool.tags

    def test_schema_generate_tool_instantiation(self):
        """SchemaGenerateTool 能正确实例化"""
        tool = SchemaGenerateTool()
        assert tool.name == "schema_generate"
        assert "generation" in tool.tags

    def test_all_infra_tools_registered_in_registry(self):
        """所有基础设施工具都能注册到 ToolRegistry"""
        registry = ToolRegistry()
        registry.register(BaiduSearchTool())
        registry.register(WebCrawlTool())
        registry.register(SchemaExtractTool())
        registry.register(SchemaGenerateTool())

        assert registry.has_tool("baidu_search")
        assert registry.has_tool("web_crawl")
        assert registry.has_tool("schema_extract")
        assert registry.has_tool("schema_generate")


# ── Test: AgentConfig tools 字段向后兼容 ──────────────────────


class TestAgentConfigToolsBackwardCompat:
    """验证 AgentConfig 的 tools 字段向后兼容"""

    def test_agent_config_with_tools_list(self):
        """AgentConfig 接受 tools 列表"""
        config = AgentConfig(
            name="test",
            agent_type="test",
            task_mode="tool_call",
            tools=["baidu_search", "web_crawl"],
        )
        assert config.tools == ["baidu_search", "web_crawl"]

    def test_agent_config_without_tools(self):
        """AgentConfig 不提供 tools 时默认为空列表"""
        config = AgentConfig(
            name="test",
            agent_type="test",
            task_mode="llm_generate",
            prompt={"identity": "test", "instructions": "test"},
        )
        assert config.tools == []

    def test_skill_config_inherits_tools(self):
        """SkillConfig 继承 AgentConfig 的 tools 字段"""
        config = SkillConfig(
            name="test",
            agent_type="test",
            task_mode="tool_call",
            tools=["baidu_search"],
        )
        assert config.tools == ["baidu_search"]

    def test_skill_config_from_dict_with_tools(self):
        """SkillConfig.from_dict 正确解析 tools 字段"""
        data = {
            "name": "test",
            "agent_type": "test",
            "task_mode": "tool_call",
            "tools": ["baidu_search", "web_crawl", "schema_generate"],
        }
        config = SkillConfig.from_dict(data)
        assert config.tools == ["baidu_search", "web_crawl", "schema_generate"]
