"""
Comprehensive pytest test suite for Agentbyte tool system (Chapter 4.6.4).

Tests cover:
- FunctionTool wrapping and schema generation
- Async function support
- Parameter validation
- Error handling
- Literal types and Enum support
- @tool decorator (with and without parentheses)
- LLM format compatibility

Run with: uv run pytest tests/tools/test_tools.py -v
"""
import asyncio
from enum import Enum
from typing import Literal

import pytest

from agentbyte.tools import ApprovalMode, FunctionTool, tool


# ============================================================================
# Test Fixtures
# ============================================================================


class Color(Enum):
    """Test enum for Enum support in schema generation."""
    RED = "red"
    GREEN = "green"
    BLUE = "blue"


def simple_add(a: int, b: int) -> int:
    """Add two numbers."""
    return a + b


def greet(name: str, greeting: str = "Hello") -> str:
    """Greet someone with optional custom greeting."""
    return f"{greeting}, {name}!"


async def async_multiply(x: int, y: int) -> int:
    """Asynchronously multiply two numbers."""
    await asyncio.sleep(0.01)
    return x * y


def process_action(action: Literal["start", "stop", "pause"]) -> str:
    """Process an action with Literal type validation."""
    return f"Action '{action}' processed"


def divide_numbers(a: float, b: float) -> float:
    """Divide two numbers (can raise ZeroDivisionError)."""
    return a / b


# ============================================================================
# Tests: FunctionTool Basic Usage
# ============================================================================


class TestFunctionToolBasic:
    """Test basic FunctionTool functionality."""

    def test_function_tool_creation(self):
        """Test creating a FunctionTool from a function."""
        tool_instance = FunctionTool(simple_add)
        assert tool_instance.name == "simple_add"
        assert tool_instance.description == "Add two numbers."
        assert tool_instance.version == "1.0.0"
        assert tool_instance.approval_mode == ApprovalMode.NEVER

    def test_function_tool_custom_metadata(self):
        """Test FunctionTool with custom name and description."""
        tool_instance = FunctionTool(
            simple_add,
            name="addition_tool",
            description="Adds two integers",
            version="2.0.0",
            approval_mode=ApprovalMode.ALWAYS,
        )
        assert tool_instance.name == "addition_tool"
        assert tool_instance.description == "Adds two integers"
        assert tool_instance.version == "2.0.0"
        assert tool_instance.approval_mode == ApprovalMode.ALWAYS

    def test_function_tool_direct_calling(self):
        """Test that wrapped functions remain callable."""
        tool_instance = FunctionTool(simple_add)
        result = tool_instance(5, 3)
        assert result == 8

    def test_function_tool_str_repr(self):
        """Test string representations."""
        tool_instance = FunctionTool(simple_add)
        assert "simple_add" in str(tool_instance)
        assert "simple_add" in repr(tool_instance)


# ============================================================================
# Tests: Parameter Schemas
# ============================================================================


class TestFunctionToolSchema:
    """Test JSON schema generation."""

    def test_schema_required_parameters(self):
        """Test schema correctly identifies required parameters."""
        tool_instance = FunctionTool(simple_add)
        schema = tool_instance.parameters

        assert schema["type"] == "object"
        assert set(schema["required"]) == {"a", "b"}
        assert all(param in schema["properties"] for param in ["a", "b"])

    def test_schema_optional_parameters(self):
        """Test schema correctly identifies optional parameters."""
        tool_instance = FunctionTool(greet)
        schema = tool_instance.parameters

        assert "name" in schema["required"]
        assert "greeting" not in schema["required"]

    def test_schema_type_mapping(self):
        """Test Python types are correctly mapped to JSON schema types."""
        tool_instance = FunctionTool(simple_add)
        schema = tool_instance.parameters

        assert schema["properties"]["a"]["type"] == "integer"
        assert schema["properties"]["b"]["type"] == "integer"

    def test_schema_literal_types(self):
        """Test Literal types generate enum constraints."""
        tool_instance = FunctionTool(process_action)
        schema = tool_instance.parameters

        action_prop = schema["properties"]["action"]
        assert "enum" in action_prop
        assert set(action_prop["enum"]) == {"start", "stop", "pause"}

    def test_schema_float_type(self):
        """Test float type mapping."""
        tool_instance = FunctionTool(divide_numbers)
        schema = tool_instance.parameters

        assert schema["properties"]["a"]["type"] == "number"
        assert schema["properties"]["b"]["type"] == "number"


# ============================================================================
# Tests: LLM Format Conversion
# ============================================================================


class TestLLMFormat:
    """Test conversion to LLM-compatible formats."""

    def test_llm_format_structure(self):
        """Test LLM format has correct structure."""
        tool_instance = FunctionTool(simple_add)
        llm_format = tool_instance.to_llm_format()

        assert llm_format["type"] == "function"
        assert "function" in llm_format
        assert "name" in llm_format["function"]
        assert "description" in llm_format["function"]
        assert "parameters" in llm_format["function"]

    def test_llm_format_versioning(self):
        """Test tool name includes version when not default."""
        tool1 = FunctionTool(simple_add, version="1.0.0")
        assert tool1.to_llm_format()["function"]["name"] == "simple_add"

        tool2 = FunctionTool(simple_add, version="2.1.0")
        assert tool2.to_llm_format()["function"]["name"] == "simple_add_v2.1.0"

    def test_llm_format_openai_compatible(self):
        """Test LLM format is OpenAI function calling compatible."""
        tool_instance = FunctionTool(simple_add, description="Add numbers")
        llm_format = tool_instance.to_llm_format()

        assert llm_format["type"] == "function"
        function = llm_format["function"]
        assert function["name"]
        assert function["description"]
        assert function["parameters"]["type"] == "object"


# ============================================================================
# Tests: Parameter Validation
# ============================================================================


class TestParameterValidation:
    """Test parameter validation before execution."""

    def test_validate_parameters_valid(self):
        """Test validation passes for valid parameters."""
        tool_instance = FunctionTool(simple_add)
        assert tool_instance.validate_parameters({"a": 5, "b": 3}) is True

    def test_validate_parameters_missing_required(self):
        """Test validation fails when required parameter is missing."""
        tool_instance = FunctionTool(simple_add)
        assert tool_instance.validate_parameters({"a": 5}) is False

    def test_validate_parameters_wrong_type(self):
        """Test validation fails for wrong parameter type."""
        tool_instance = FunctionTool(simple_add)
        assert tool_instance.validate_parameters({"a": "not_a_number", "b": 3}) is False

    def test_validate_parameters_optional_missing(self):
        """Test validation passes when optional parameter is missing."""
        tool_instance = FunctionTool(greet)
        assert tool_instance.validate_parameters({"name": "Alice"}) is True

    def test_validate_parameters_extra_params(self):
        """Test validation with extra parameters (should still pass)."""
        tool_instance = FunctionTool(simple_add)
        assert tool_instance.validate_parameters({"a": 5, "b": 3, "extra": "ignored"}) is True


# ============================================================================
# Tests: Tool Execution
# ============================================================================


class TestFunctionToolExecution:
    """Test tool execution with sync and async functions."""

    @pytest.mark.asyncio
    async def test_execute_sync_function(self):
        """Test executing a synchronous function."""
        tool_instance = FunctionTool(simple_add)
        result = await tool_instance.execute({"a": 5, "b": 3})

        assert result.success is True
        assert result.result == 8
        assert result.error is None
        assert result.metadata["tool_name"] == "simple_add"

    @pytest.mark.asyncio
    async def test_execute_async_function(self):
        """Test executing an asynchronous function."""
        tool_instance = FunctionTool(async_multiply)
        result = await tool_instance.execute({"x": 4, "y": 6})

        assert result.success is True
        assert result.result == 24
        assert result.error is None

    @pytest.mark.asyncio
    async def test_execute_with_optional_params(self):
        """Test execution with optional parameters."""
        tool_instance = FunctionTool(greet)

        result1 = await tool_instance.execute({"name": "Alice"})
        assert result1.success is True
        assert result1.result == "Hello, Alice!"

        result2 = await tool_instance.execute({"name": "Bob", "greeting": "Hi"})
        assert result2.success is True
        assert result2.result == "Hi, Bob!"

    @pytest.mark.asyncio
    async def test_execute_invalid_parameters(self):
        """Test execution fails gracefully with invalid parameters."""
        tool_instance = FunctionTool(simple_add)
        result = await tool_instance.execute({"a": 5})

        assert result.success is False
        assert result.error is not None
        assert "Invalid parameters" in result.error

    @pytest.mark.asyncio
    async def test_execute_exception_handling(self):
        """Test execution catches exceptions and returns ToolResult."""
        tool_instance = FunctionTool(divide_numbers)
        result = await tool_instance.execute({"a": 10, "b": 0})

        assert result.success is False
        assert result.error is not None
        assert "division" in result.error.lower()
        assert result.metadata["exception_type"] == "ZeroDivisionError"


# ============================================================================
# Tests: @tool Decorator
# ============================================================================


class TestToolDecorator:
    """Test @tool decorator functionality."""

    def test_decorator_without_parentheses(self):
        """Test @tool without parentheses."""
        @tool
        def add(x: int, y: int) -> int:
            """Add two numbers."""
            return x + y

        assert isinstance(add, FunctionTool)
        assert add.name == "add"
        assert add.approval_mode == ApprovalMode.NEVER

    def test_decorator_with_parentheses(self):
        """Test @tool(...) with parameters."""
        @tool(name="custom_adder", description="Custom add function")
        def add(x: int, y: int) -> int:
            """Add two numbers."""
            return x + y

        assert isinstance(add, FunctionTool)
        assert add.name == "custom_adder"
        assert add.description == "Custom add function"

    def test_decorator_approval_mode_string(self):
        """Test @tool with approval_mode as string."""
        @tool(approval_mode="always_require")
        def sensitive_op() -> str:
            """Sensitive operation."""
            return "done"

        assert sensitive_op.approval_mode == ApprovalMode.ALWAYS

    def test_decorator_approval_mode_enum(self):
        """Test @tool with approval_mode as enum."""
        @tool(approval_mode=ApprovalMode.ALWAYS)
        def sensitive_op() -> str:
            """Sensitive operation."""
            return "done"

        assert sensitive_op.approval_mode == ApprovalMode.ALWAYS

    def test_decorator_preserves_callability(self):
        """Test decorated function remains callable."""
        @tool
        def add(x: int, y: int) -> int:
            """Add two numbers."""
            return x + y

        result = add(5, 3)
        assert result == 8

    @pytest.mark.asyncio
    async def test_decorator_async_function(self):
        """Test @tool works with async functions."""
        @tool
        async def async_task(delay: float) -> str:
            """Async task."""
            await asyncio.sleep(delay)
            return "completed"

        assert isinstance(async_task, FunctionTool)
        result = await async_task.execute({"delay": 0.01})
        assert result.success is True


# ============================================================================
# Tests: Type Conversion and Edge Cases
# ============================================================================


class TestTypeConversion:
    """Test Python type to JSON schema type conversion."""

    def test_type_str(self):
        """Test string type conversion."""
        @tool
        def tool_func(text: str) -> str:
            return text

        schema = tool_func.parameters
        assert schema["properties"]["text"]["type"] == "string"

    def test_type_bool(self):
        """Test boolean type conversion."""
        @tool
        def tool_func(flag: bool) -> bool:
            return flag

        schema = tool_func.parameters
        assert schema["properties"]["flag"]["type"] == "boolean"

    def test_type_list(self):
        """Test list type conversion."""
        @tool
        def tool_func(items: list) -> list:
            return items

        schema = tool_func.parameters
        assert schema["properties"]["items"]["type"] == "array"

    def test_type_dict(self):
        """Test dict type conversion."""
        @tool
        def tool_func(data: dict) -> dict:
            return data

        schema = tool_func.parameters
        assert schema["properties"]["data"]["type"] == "object"


# ============================================================================
# Tests: Integration
# ============================================================================


class TestIntegration:
    """Integration tests combining multiple features."""

    @pytest.mark.asyncio
    async def test_full_workflow(self):
        """Test complete workflow: decorator -> schema -> validation -> execution."""
        @tool(description="Multiply two numbers")
        def multiply(x: int, y: int) -> int:
            """Multiply x and y."""
            return x * y

        assert multiply.name == "multiply"
        assert multiply.description == "Multiply two numbers"

        schema = multiply.parameters
        assert set(schema["required"]) == {"x", "y"}

        llm_format = multiply.to_llm_format()
        assert llm_format["function"]["name"] == "multiply"

        assert multiply.validate_parameters({"x": 5, "y": 3}) is True

        result = await multiply.execute({"x": 5, "y": 3})
        assert result.success is True
        assert result.result == 15

    @pytest.mark.asyncio
    async def test_multiple_tools_for_agent(self):
        """Test creating multiple tools for an agent."""
        @tool(description="Add numbers")
        def add(a: int, b: int) -> int:
            return a + b

        @tool(description="Subtract numbers")
        def subtract(a: int, b: int) -> int:
            return a - b

        @tool(description="Multiply numbers")
        def multiply(a: int, b: int) -> int:
            return a * b

        tools_list = [add, subtract, multiply]

        results = [
            await tool.execute({"a": 10, "b": 5})
            for tool in tools_list
        ]

        assert results[0].result == 15
        assert results[1].result == 5
        assert results[2].result == 50
