"""Tests for ReAct Prompt, Skill/Agent tool sync, MCP bridge, and execution modes"""

import asyncio
import json
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch

import pytest

from agentkit.core.config_driven import AgentConfig, ConfigDrivenAgent
from agentkit.core.protocol import TaskMessage, TaskStatus
from agentkit.skills.base import Skill, SkillConfig
from agentkit.tools.function_tool import FunctionTool
from agentkit.tools.registry import ToolRegistry


def _make_skill_config(execution_mode="react", **kwargs) -> SkillConfig:
    """Helper to create a SkillConfig for testing."""
    defaults = {
        "name": "test_skill",
        "agent_type": "test",
        "task_mode": "llm_generate",
        "supported_tasks": ["test_task"],
        "prompt": {
            "identity": "You are a test assistant.",
            "context": "Context: ${topic}",
            "instructions": "Please help with: ${query}",
            "constraints": "Be concise.",
            "output_format": "Return JSON.",
            "examples": "Example: input -> output",
        },
        "execution_mode": execution_mode,
        "max_steps": 3,
        "intent": {
            "keywords": ["test", "demo"],
            "description": "A test skill",
        },
    }
    defaults.update(kwargs)
    return SkillConfig.from_dict(defaults)


def _make_task(**kwargs) -> TaskMessage:
    """Helper to create a TaskMessage for testing."""
    defaults = {
        "task_id": "test-task-1",
        "agent_name": "test_skill",
        "task_type": "test_task",
        "priority": 0,
        "input_data": {"topic": "AI", "query": "What is AI?"},
        "callback_url": None,
        "created_at": datetime.now(timezone.utc),
    }
    defaults.update(kwargs)
    return TaskMessage(**defaults)


class TestReActPromptFullRendering:
    """Test that ReAct mode uses full PromptTemplate.render() output."""

    @pytest.mark.asyncio
    async def test_react_uses_full_prompt_template(self):
        """ReAct mode should use PromptTemplate.render() to get all prompt sections,
        not just identity.

        In ReAct mode, _handle_react() passes system_prompt to ReActEngine.execute(),
        which prepends it as a system message in the conversation passed to gateway.chat().
        So we check the 'messages' kwarg for a system message containing all sections.
        """
        config = _make_skill_config(execution_mode="react")
        tool_registry = ToolRegistry()

        # Mock LLMGateway to capture what messages are sent
        mock_gateway = MagicMock()
        mock_response = MagicMock()
        mock_response.content = json.dumps({"answer": "test"})
        mock_response.usage = MagicMock()
        mock_response.usage.total_tokens = 10
        mock_response.has_tool_calls = False
        mock_gateway.chat = AsyncMock(return_value=mock_response)

        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry,
            llm_gateway=mock_gateway,
        )

        task = _make_task()
        await agent.handle_task(task)

        # Verify the gateway was called
        mock_gateway.chat.assert_called_once()
        call_kwargs = mock_gateway.chat.call_args

        # ReActEngine.execute() puts system_prompt as the first message in conversation
        messages = call_kwargs.kwargs.get("messages", [])
        assert len(messages) > 0, "No messages sent to gateway"

        # First message should be the system message with all prompt sections
        system_msg = messages[0]
        assert system_msg["role"] == "system", f"First message is not system: {system_msg['role']}"
        system_content = system_msg["content"]
        assert "test assistant" in system_content, f"Identity missing from system message: {system_content}"
        assert "AI" in system_content, f"Context variable not resolved in system message: {system_content}"
        assert "concise" in system_content, f"Constraints missing from system message: {system_content}"

        # Check that user messages contain instructions + output_format + examples
        user_content = " ".join(m.get("content", "") for m in messages if m["role"] != "system")
        assert "What is AI?" in user_content, f"Instructions variable not resolved: {user_content}"
        assert "JSON" in user_content, f"Output format missing: {user_content}"
        assert "Example" in user_content, f"Examples missing: {user_content}"

    @pytest.mark.asyncio
    async def test_react_without_prompt_template(self):
        """ReAct mode without prompt template should use input_data as fallback."""
        config = SkillConfig(
            name="no_prompt_skill",
            agent_type="test",
            task_mode="tool_call",
            supported_tasks=["test"],
            execution_mode="react",
            tools=["mock_tool"],
        )
        tool_registry = ToolRegistry()

        async def mock_func(**kwargs):
            return {"mock": True}

        tool_registry.register(FunctionTool(name="mock_tool", description="Mock", func=mock_func))

        mock_gateway = MagicMock()
        mock_response = MagicMock()
        mock_response.content = '{"result": "ok"}'
        mock_response.usage = MagicMock()
        mock_response.usage.total_tokens = 5
        mock_response.has_tool_calls = False
        mock_gateway.chat = AsyncMock(return_value=mock_response)

        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry,
            llm_gateway=mock_gateway,
        )

        task = _make_task(input_data={"message": "hello"})
        result = await agent.handle_task(task)
        assert isinstance(result, dict)


class TestSkillAgentToolSync:
    """Test that Skill-bound tools are merged into Agent._tools."""

    def test_skill_tools_merged_into_agent(self):
        """When ConfigDrivenAgent receives a SkillConfig with tools,
        the Skill's bound tools should be merged into Agent._tools."""
        config = _make_skill_config(
            execution_mode="react",
            tools=["tool_a", "tool_b"],
        )
        tool_registry = ToolRegistry()

        async def mock_func(**kwargs):
            return {"mock": True}

        tool_registry.register(FunctionTool(name="tool_a", description="Tool A", func=mock_func))
        tool_registry.register(FunctionTool(name="tool_b", description="Tool B", func=mock_func))

        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry,
        )

        # Agent should have both tools from the config
        tool_names = [t.name for t in agent._tools]
        assert "tool_a" in tool_names, f"tool_a not found in agent tools: {tool_names}"
        assert "tool_b" in tool_names, f"tool_b not found in agent tools: {tool_names}"

    def test_skill_instance_tools_merged(self):
        """When a Skill instance has tools bound via bind_tool(),
        those tools should be merged into Agent._tools."""
        config = _make_skill_config(execution_mode="react")
        tool_registry = ToolRegistry()

        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry,
        )

        # Manually bind a tool to the skill instance
        async def extra_func(**kwargs):
            return {"extra": True}

        extra_tool = FunctionTool(name="extra_tool", description="Extra", func=extra_func)
        agent._skill_instance.bind_tool(extra_tool)

        # Simulate re-creating agent (in real flow, tools are merged during __init__)
        # For this test, verify the merge logic works
        initial_count = len(agent._tools)
        for tool in agent._skill_instance.tools:
            if not any(t.name == tool.name for t in agent._tools):
                agent.use_tool(tool)

        tool_names = [t.name for t in agent._tools]
        assert "extra_tool" in tool_names
        assert len(agent._tools) == initial_count + 1


class TestMCPBridge:
    """Test MCP → ReAct bridge."""

    @pytest.mark.asyncio
    async def test_mcp_servers_parameter_accepted(self):
        """ConfigDrivenAgent should accept mcp_servers parameter."""
        config = _make_skill_config(execution_mode="react")
        tool_registry = ToolRegistry()

        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry,
            mcp_servers={"test_server": "http://localhost:8080"},
        )

        assert agent._mcp_servers == {"test_server": "http://localhost:8080"}
        assert agent._mcp_tools_registered is False

    @pytest.mark.asyncio
    async def test_mcp_lazy_registration_on_task(self):
        """MCP tools should be lazily registered on first task execution."""
        config = _make_skill_config(execution_mode="react")
        tool_registry = ToolRegistry()

        mock_gateway = MagicMock()
        mock_response = MagicMock()
        mock_response.content = '{"result": "ok"}'
        mock_response.usage = MagicMock()
        mock_response.usage.total_tokens = 5
        mock_response.has_tool_calls = False
        mock_gateway.chat = AsyncMock(return_value=mock_response)

        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry,
            llm_gateway=mock_gateway,
            mcp_servers={"test_server": "http://localhost:8080"},
        )

        # Mock MCPClient to avoid real HTTP calls
        with patch("agentkit.mcp.client.MCPClient") as MockMCPClient:
            mock_client_instance = MagicMock()
            mock_client_instance.list_tools = AsyncMock(return_value=[
                {"name": "remote_tool", "description": "A remote tool"}
            ])
            mock_mcp_tool = MagicMock()
            mock_mcp_tool.name = "remote_tool"
            mock_client_instance.as_tool = MagicMock(return_value=mock_mcp_tool)
            MockMCPClient.return_value = mock_client_instance

            task = _make_task()
            await agent.handle_task(task)

            # MCP tools should now be registered
            assert agent._mcp_tools_registered is True
            mock_client_instance.list_tools.assert_called_once()

    @pytest.mark.asyncio
    async def test_mcp_registration_failure_graceful(self):
        """MCP registration failure should not prevent task execution."""
        config = _make_skill_config(execution_mode="react")
        tool_registry = ToolRegistry()

        mock_gateway = MagicMock()
        mock_response = MagicMock()
        mock_response.content = '{"result": "ok"}'
        mock_response.usage = MagicMock()
        mock_response.usage.total_tokens = 5
        mock_response.has_tool_calls = False
        mock_gateway.chat = AsyncMock(return_value=mock_response)

        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry,
            llm_gateway=mock_gateway,
            mcp_servers={"bad_server": "http://nonexistent:9999"},
        )

        with patch("agentkit.mcp.client.MCPClient") as MockMCPClient:
            MockMCPClient.return_value.list_tools = AsyncMock(
                side_effect=Exception("Connection refused")
            )

            task = _make_task()
            result = await agent.handle_task(task)
            # Should still complete despite MCP failure
            assert isinstance(result, dict)


class TestExecutionModes:
    """Test execution_mode=react/direct/custom."""

    @pytest.mark.asyncio
    async def test_direct_mode_single_llm_call(self):
        """execution_mode=direct should make a single LLM call without ReAct loop."""
        config = _make_skill_config(execution_mode="direct")
        tool_registry = ToolRegistry()

        mock_gateway = MagicMock()
        mock_response = MagicMock()
        mock_response.content = json.dumps({"answer": "direct result"})
        mock_response.usage = MagicMock()
        mock_response.usage.total_tokens = 15
        mock_gateway.chat = AsyncMock(return_value=mock_response)

        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry,
            llm_gateway=mock_gateway,
        )

        task = _make_task()
        result = await agent.handle_task(task)

        # Should call gateway.chat directly (not ReAct engine)
        mock_gateway.chat.assert_called_once()
        assert result == {"answer": "direct result"}

    @pytest.mark.asyncio
    async def test_custom_mode_with_skill_config(self):
        """execution_mode=custom should use custom handler."""
        config = _make_skill_config(
            execution_mode="custom",
            custom_handler="test.handlers.mock_handler",
        )
        tool_registry = ToolRegistry()

        async def mock_handler(task):
            return {"custom": True, "task_id": task.task_id}

        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry,
            custom_handlers={"test.handlers.mock_handler": mock_handler},
        )

        task = _make_task()
        result = await agent.handle_task(task)

        assert result["custom"] is True
        assert result["task_id"] == "test-task-1"

    @pytest.mark.asyncio
    async def test_react_mode_uses_react_engine(self):
        """execution_mode=react should use ReAct engine."""
        config = _make_skill_config(execution_mode="react")
        tool_registry = ToolRegistry()

        mock_gateway = MagicMock()
        mock_response = MagicMock()
        mock_response.content = json.dumps({"answer": "react result"})
        mock_response.usage = MagicMock()
        mock_response.usage.total_tokens = 20
        mock_response.has_tool_calls = False
        mock_gateway.chat = AsyncMock(return_value=mock_response)

        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry,
            llm_gateway=mock_gateway,
        )

        task = _make_task()
        result = await agent.handle_task(task)

        assert isinstance(result, dict)

    @pytest.mark.asyncio
    async def test_fallback_to_task_mode_without_skill_config(self):
        """Without SkillConfig, should fall back to task_mode."""
        config = AgentConfig(
            name="legacy_agent",
            agent_type="test",
            task_mode="llm_generate",
            supported_tasks=["test"],
            prompt={"identity": "Legacy agent"},
        )
        tool_registry = ToolRegistry()

        agent = ConfigDrivenAgent(
            config=config,
            tool_registry=tool_registry,
        )

        task = _make_task()
        result = await agent.handle_task(task)

        # Should return rendered prompt (no LLM client)
        assert "messages" in result or isinstance(result, dict)
