"""Tests for OpenAI converter utilities."""

from typing import Annotated

import pytest
from arcade_core.catalog import MaterializedTool, ToolMeta, create_func_models
from arcade_core.converters.openai import (
    OpenAIFunctionParameterProperty,
    OpenAIFunctionParameters,
    OpenAIFunctionSchema,
    OpenAIToolSchema,
    _convert_input_parameters_to_json_schema,
    _convert_value_schema_to_json_schema,
    _create_tool_schema,
    to_openai,
)
from arcade_core.schema import (
    InputParameter,
    ToolDefinition,
    ToolInput,
    ToolkitDefinition,
    ToolOutput,
    ToolRequirements,
    ValueSchema,
)


class TestOpenAIConverter:
    """Test OpenAI converter functions."""

    @pytest.fixture
    def sample_tool_def(self):
        """Create a sample tool definition."""
        return ToolDefinition(
            name="calculate",
            fully_qualified_name="MathToolkit.calculate",
            description="Perform a calculation",
            toolkit=ToolkitDefinition(
                name="MathToolkit",
                description="Math tools",
                version="1.0.0",
            ),
            input=ToolInput(
                parameters=[
                    InputParameter(
                        name="expression",
                        required=True,
                        description="Math expression to evaluate",
                        value_schema=ValueSchema(val_type="string"),
                    ),
                    InputParameter(
                        name="precision",
                        required=False,
                        description="Decimal precision",
                        value_schema=ValueSchema(val_type="integer"),
                    ),
                ]
            ),
            output=ToolOutput(
                description="Calculation result",
                value_schema=ValueSchema(val_type="number"),
            ),
            requirements=ToolRequirements(),
        )

    @pytest.fixture
    def materialized_tool(self, sample_tool_def):
        """Create a materialized tool."""

        def calculate(
            expression: Annotated[str, "Math expression"] = "1 + 1",
            precision: Annotated[int, "Decimal precision"] = 2,
        ) -> Annotated[float, "Calculation result"]:
            """Perform a calculation."""
            return round(eval(expression), precision)  # noqa: S307

        input_model, output_model = create_func_models(calculate)
        meta = ToolMeta(module=calculate.__module__, toolkit=sample_tool_def.toolkit.name)
        return MaterializedTool(
            tool=calculate,
            definition=sample_tool_def,
            meta=meta,
            input_model=input_model,
            output_model=output_model,
        )

    def test_to_openai_basic(self, materialized_tool):
        """Test basic OpenAI tool conversion."""
        result = to_openai(materialized_tool)

        assert isinstance(result, dict)
        assert result["type"] == "function"
        assert "function" in result

        function = result["function"]
        assert function["name"] == "MathToolkit_calculate"
        assert function["description"] == "Perform a calculation"
        assert function["strict"] is True
        assert "parameters" in function

    def test_function_name_conversion(self, materialized_tool):
        """Test that dots in fully_qualified_name are converted to underscores."""
        result = to_openai(materialized_tool)
        assert result["function"]["name"] == "MathToolkit_calculate"

    def test_function_parameters_structure(self, materialized_tool):
        """Test the structure of function parameters."""
        result = to_openai(materialized_tool)
        params = result["function"]["parameters"]

        assert params["type"] == "object"
        assert params["additionalProperties"] is False
        assert "properties" in params
        assert "required" in params

        # All parameters should be in required list for strict mode
        assert set(params["required"]) == {"expression", "precision"}

    def test_required_parameter_schema(self, materialized_tool):
        """Test required parameter schema generation."""
        result = to_openai(materialized_tool)
        props = result["function"]["parameters"]["properties"]

        expression_prop = props["expression"]
        assert expression_prop["type"] == "string"
        assert expression_prop["description"] == "Math expression to evaluate"

    def test_optional_parameter_schema(self, materialized_tool):
        """Test optional parameter schema with null union type."""
        result = to_openai(materialized_tool)
        props = result["function"]["parameters"]["properties"]

        precision_prop = props["precision"]
        # Optional parameters should have union type with null
        assert precision_prop["type"] == ["integer", "null"]
        assert precision_prop["description"] == "Decimal precision"

    def test_no_parameters_tool(self):
        """Test tool with no parameters."""
        tool_def = ToolDefinition(
            name="get_time",
            fully_qualified_name="TimeToolkit.get_time",
            description="Get current time",
            toolkit=ToolkitDefinition(name="TimeToolkit"),
            input=ToolInput(parameters=[]),
            output=ToolOutput(),
            requirements=ToolRequirements(),
        )

        def get_time() -> Annotated[str, "current time"]:
            return "2023-01-01T00:00:00Z"

        input_model, output_model = create_func_models(get_time)
        meta = ToolMeta(module=get_time.__module__, toolkit=tool_def.toolkit.name)
        mat_tool = MaterializedTool(
            tool=get_time,
            definition=tool_def,
            meta=meta,
            input_model=input_model,
            output_model=output_model,
        )

        result = to_openai(mat_tool)
        params = result["function"]["parameters"]

        assert params["type"] == "object"
        assert params["properties"] == {}
        assert params["additionalProperties"] is False
        # No required field when there are no parameters
        assert "required" not in params

    @pytest.mark.parametrize(
        "arcade_type,expected_json_type",
        [
            ("string", "string"),
            ("integer", "integer"),
            ("number", "number"),
            ("boolean", "boolean"),
            ("array", "array"),
            ("json", "object"),
        ],
    )
    def test_parameter_type_conversion(self, arcade_type, expected_json_type):
        """Test different parameter type conversions."""
        tool_def = ToolDefinition(
            name="test",
            fully_qualified_name="Test.test",
            description="Test tool",
            toolkit=ToolkitDefinition(name="Test"),
            input=ToolInput(
                parameters=[
                    InputParameter(
                        name="param",
                        required=True,
                        description="Test parameter",
                        value_schema=ValueSchema(val_type=arcade_type),
                    )
                ]
            ),
            output=ToolOutput(),
            requirements=ToolRequirements(),
        )

        def test_func(param: Annotated[str, "Test parameter"]):
            return param

        input_model, output_model = create_func_models(test_func)
        meta = ToolMeta(module=test_func.__module__, toolkit=tool_def.toolkit.name)
        mat_tool = MaterializedTool(
            tool=test_func,
            definition=tool_def,
            meta=meta,
            input_model=input_model,
            output_model=output_model,
        )

        result = to_openai(mat_tool)
        param_schema = result["function"]["parameters"]["properties"]["param"]
        assert param_schema["type"] == expected_json_type

    def test_array_parameter_with_inner_type(self):
        """Test array parameter with inner type specification."""
        tool_def = ToolDefinition(
            name="process_items",
            fully_qualified_name="ArrayToolkit.process_items",
            description="Process a list of items",
            toolkit=ToolkitDefinition(name="ArrayToolkit"),
            input=ToolInput(
                parameters=[
                    InputParameter(
                        name="items",
                        required=True,
                        description="List of string items",
                        value_schema=ValueSchema(
                            val_type="array",
                            inner_val_type="string",
                        ),
                    )
                ]
            ),
            output=ToolOutput(),
            requirements=ToolRequirements(),
        )

        def process_items(items: Annotated[list[str], "List of string items"]):
            return items

        input_model, output_model = create_func_models(process_items)
        meta = ToolMeta(module=process_items.__module__, toolkit=tool_def.toolkit.name)
        mat_tool = MaterializedTool(
            tool=process_items,
            definition=tool_def,
            meta=meta,
            input_model=input_model,
            output_model=output_model,
        )

        result = to_openai(mat_tool)
        param_schema = result["function"]["parameters"]["properties"]["items"]

        assert param_schema["type"] == "array"
        assert param_schema["items"]["type"] == "string"

    def test_enum_parameter(self):
        """Test parameter with enum values."""
        tool_def = ToolDefinition(
            name="set_color",
            fully_qualified_name="ColorToolkit.set_color",
            description="Set a color",
            toolkit=ToolkitDefinition(name="ColorToolkit"),
            input=ToolInput(
                parameters=[
                    InputParameter(
                        name="color",
                        required=True,
                        description="Color choice",
                        value_schema=ValueSchema(
                            val_type="string",
                            enum=["red", "green", "blue"],
                        ),
                    )
                ]
            ),
            output=ToolOutput(),
            requirements=ToolRequirements(),
        )

        def set_color(color: Annotated[str, "Color choice"]):
            return color

        input_model, output_model = create_func_models(set_color)
        meta = ToolMeta(module=set_color.__module__, toolkit=tool_def.toolkit.name)
        mat_tool = MaterializedTool(
            tool=set_color,
            definition=tool_def,
            meta=meta,
            input_model=input_model,
            output_model=output_model,
        )

        result = to_openai(mat_tool)
        param_schema = result["function"]["parameters"]["properties"]["color"]

        assert param_schema["type"] == "string"
        assert param_schema["enum"] == ["red", "green", "blue"]

    def test_array_with_enum_items(self):
        """Test array parameter where items have enum values."""
        tool_def = ToolDefinition(
            name="set_colors",
            fully_qualified_name="ColorToolkit.set_colors",
            description="Set multiple colors",
            toolkit=ToolkitDefinition(name="ColorToolkit"),
            input=ToolInput(
                parameters=[
                    InputParameter(
                        name="colors",
                        required=True,
                        description="List of colors",
                        value_schema=ValueSchema(
                            val_type="array",
                            inner_val_type="string",
                            enum=["red", "green", "blue"],
                        ),
                    )
                ]
            ),
            output=ToolOutput(),
            requirements=ToolRequirements(),
        )

        def set_colors(colors: Annotated[list[str], "List of colors"]):
            return colors

        input_model, output_model = create_func_models(set_colors)
        meta = ToolMeta(module=set_colors.__module__, toolkit=tool_def.toolkit.name)
        mat_tool = MaterializedTool(
            tool=set_colors,
            definition=tool_def,
            meta=meta,
            input_model=input_model,
            output_model=output_model,
        )

        result = to_openai(mat_tool)
        param_schema = result["function"]["parameters"]["properties"]["colors"]

        assert param_schema["type"] == "array"
        assert param_schema["items"]["type"] == "string"
        assert param_schema["items"]["enum"] == ["red", "green", "blue"]

    def test_json_parameter_with_properties(self):
        """Test JSON parameter with nested properties."""
        tool_def = ToolDefinition(
            name="create_user",
            fully_qualified_name="UserToolkit.create_user",
            description="Create a user",
            toolkit=ToolkitDefinition(name="UserToolkit"),
            input=ToolInput(
                parameters=[
                    InputParameter(
                        name="user_data",
                        required=True,
                        description="User information",
                        value_schema=ValueSchema(
                            val_type="json",
                            properties={
                                "name": ValueSchema(val_type="string"),
                                "age": ValueSchema(val_type="integer"),
                                "active": ValueSchema(val_type="boolean"),
                            },
                        ),
                    )
                ]
            ),
            output=ToolOutput(),
            requirements=ToolRequirements(),
        )

        def create_user(user_data: Annotated[dict, "User information"]):
            return user_data

        input_model, output_model = create_func_models(create_user)
        meta = ToolMeta(module=create_user.__module__, toolkit=tool_def.toolkit.name)
        mat_tool = MaterializedTool(
            tool=create_user,
            definition=tool_def,
            meta=meta,
            input_model=input_model,
            output_model=output_model,
        )

        result = to_openai(mat_tool)
        param_schema = result["function"]["parameters"]["properties"]["user_data"]

        assert param_schema["type"] == "object"
        assert "properties" in param_schema
        assert param_schema["properties"]["name"]["type"] == "string"
        assert param_schema["properties"]["age"]["type"] == "integer"
        assert param_schema["properties"]["active"]["type"] == "boolean"

    def test_multiple_optional_parameters(self):
        """Test tool with multiple optional parameters."""
        tool_def = ToolDefinition(
            name="search",
            fully_qualified_name="SearchToolkit.search",
            description="Search with filters",
            toolkit=ToolkitDefinition(name="SearchToolkit"),
            input=ToolInput(
                parameters=[
                    InputParameter(
                        name="query",
                        required=True,
                        description="Search query",
                        value_schema=ValueSchema(val_type="string"),
                    ),
                    InputParameter(
                        name="limit",
                        required=False,
                        description="Result limit",
                        value_schema=ValueSchema(val_type="integer"),
                    ),
                    InputParameter(
                        name="include_metadata",
                        required=False,
                        description="Include metadata in results",
                        value_schema=ValueSchema(val_type="boolean"),
                    ),
                ]
            ),
            output=ToolOutput(),
            requirements=ToolRequirements(),
        )

        def search(
            query: Annotated[str, "Search query"],
            limit: Annotated[int, "Result limit"] = 10,
            include_metadata: Annotated[bool, "Include metadata"] = False,
        ):
            return f"Search results for {query}"

        input_model, output_model = create_func_models(search)
        meta = ToolMeta(module=search.__module__, toolkit=tool_def.toolkit.name)
        mat_tool = MaterializedTool(
            tool=search,
            definition=tool_def,
            meta=meta,
            input_model=input_model,
            output_model=output_model,
        )

        result = to_openai(mat_tool)
        props = result["function"]["parameters"]["properties"]

        # Required parameter should have single type
        assert props["query"]["type"] == "string"

        # Optional parameters should have union types with null
        assert props["limit"]["type"] == ["integer", "null"]
        assert props["include_metadata"]["type"] == ["boolean", "null"]

        # All parameters should be in required list for strict mode
        assert set(result["function"]["parameters"]["required"]) == {
            "query",
            "limit",
            "include_metadata",
        }


class TestHelperFunctions:
    """Test helper functions used by the converter."""

    def test_create_tool_schema(self):
        """Test _create_tool_schema helper function."""
        params: OpenAIFunctionParameters = {
            "type": "object",
            "properties": {"test": {"type": "string"}},
            "required": ["test"],
            "additionalProperties": False,
        }

        result = _create_tool_schema("test_func", "Test function", params)

        assert result["type"] == "function"
        assert result["function"]["name"] == "test_func"
        assert result["function"]["description"] == "Test function"
        assert result["function"]["parameters"] == params
        assert result["function"]["strict"] is True

    def test_convert_value_schema_to_json_schema_basic_types(self):
        """Test _convert_value_schema_to_json_schema for basic types."""
        test_cases = [
            ("string", "string"),
            ("integer", "integer"),
            ("number", "number"),
            ("boolean", "boolean"),
            ("json", "object"),
            ("array", "array"),
        ]

        for arcade_type, expected_json_type in test_cases:
            schema = ValueSchema(val_type=arcade_type)
            result = _convert_value_schema_to_json_schema(schema)
            assert result["type"] == expected_json_type

    def test_convert_value_schema_with_enum(self):
        """Test _convert_value_schema_to_json_schema with enum values."""
        schema = ValueSchema(val_type="string", enum=["a", "b", "c"])
        result = _convert_value_schema_to_json_schema(schema)

        assert result["type"] == "string"
        assert result["enum"] == ["a", "b", "c"]

    def test_convert_input_parameters_empty_list(self):
        """Test _convert_input_parameters_to_json_schema with empty parameters."""
        result = _convert_input_parameters_to_json_schema([])

        assert result["type"] == "object"
        assert result["properties"] == {}
        assert result["additionalProperties"] is False
        assert "required" not in result

    def test_convert_input_parameters_with_required_and_optional(self):
        """Test _convert_input_parameters_to_json_schema with mixed parameters."""
        params = [
            InputParameter(
                name="required_param",
                required=True,
                description="Required parameter",
                value_schema=ValueSchema(val_type="string"),
            ),
            InputParameter(
                name="optional_param",
                required=False,
                description="Optional parameter",
                value_schema=ValueSchema(val_type="integer"),
            ),
        ]

        result = _convert_input_parameters_to_json_schema(params)

        assert result["type"] == "object"
        assert result["additionalProperties"] is False
        assert set(result["required"]) == {"required_param", "optional_param"}

        # Required parameter should have single type
        assert result["properties"]["required_param"]["type"] == "string"

        # Optional parameter should have union type with null
        assert result["properties"]["optional_param"]["type"] == ["integer", "null"]
