"""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 (
    OpenAIFunctionParameters,
    _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 TestOpenAIStrictNullability:
    """Strict-mode nullability for structured objects and optional enums."""

    def test_object_with_empty_required_keys_makes_fields_nullable(self):
        """An object whose required set is known-empty (all fields defaulted) must list every
        field in `required` but render each as a nullable union so the model may omit it."""
        param = InputParameter(
            name="config",
            required=True,
            description="All-defaulted config.",
            value_schema=ValueSchema(
                val_type="json",
                properties={
                    "count": ValueSchema(val_type="integer"),
                    "label": ValueSchema(val_type="string"),
                },
                required_keys=[],
            ),
        )

        schema = _convert_input_parameters_to_json_schema([param])["properties"]["config"]

        assert schema["additionalProperties"] is False
        assert set(schema["required"]) == {"count", "label"}
        assert schema["properties"]["count"]["type"] == ["integer", "null"]
        assert schema["properties"]["label"]["type"] == ["string", "null"]

    def test_freeform_dict_param_stays_open(self):
        """A freeform dict (json with no properties and unknown shape) must stay open: no
        `properties`, no `additionalProperties: false`, no forced `required`."""
        param = InputParameter(
            name="payload",
            required=True,
            description="Arbitrary JSON.",
            value_schema=ValueSchema(val_type="json"),
        )

        schema = _convert_input_parameters_to_json_schema([param])["properties"]["payload"]

        assert schema["type"] == "object"
        assert "properties" not in schema
        assert "additionalProperties" not in schema
        assert "required" not in schema

    def test_optional_enum_param_allows_null_in_enum(self):
        """An optional enum param gets `null` in its type AND `None` appended to its enum, so
        a null value satisfies both the type and enum assertions."""
        param = InputParameter(
            name="status",
            required=False,
            description="Status choice.",
            value_schema=ValueSchema(val_type="string", enum=["open", "closed"]),
        )

        schema = _convert_input_parameters_to_json_schema([param])["properties"]["status"]

        assert schema["type"] == ["string", "null"]
        assert schema["enum"] == ["open", "closed", None]

    def test_nullable_enum_nested_field_allows_null_in_enum(self):
        """A nullable enum field inside an object gets `null` in type AND `None` in enum."""
        param = InputParameter(
            name="filter",
            required=True,
            description="Filter object.",
            value_schema=ValueSchema(
                val_type="json",
                properties={
                    "status": ValueSchema(
                        val_type="string", enum=["open", "closed"], nullable=True
                    ),
                },
                required_keys=["status"],
            ),
        )

        schema = _convert_input_parameters_to_json_schema([param])["properties"]["filter"]
        status = schema["properties"]["status"]

        assert status["type"] == ["string", "null"]
        assert status["enum"] == ["open", "closed", None]


class TestOpenAIStrictEmptyObjects:
    """A known-empty object shape ({}) must close in strict mode; a freeform dict stays open."""

    def test_empty_object_param_is_closed(self):
        """An object with a known-empty shape (properties == {}) is emitted closed: it carries
        `properties: {}`, `additionalProperties: false`, and an empty `required`."""
        param = InputParameter(
            name="cfg",
            required=True,
            description="Empty config.",
            value_schema=ValueSchema(val_type="json", properties={}, required_keys=[]),
        )

        schema = _convert_input_parameters_to_json_schema([param])["properties"]["cfg"]

        assert schema["type"] == "object"
        assert schema["properties"] == {}
        assert schema["additionalProperties"] is False
        assert schema["required"] == []

    def test_array_of_empty_object_items_are_closed(self):
        """`list[<empty object>]` item schemas close the same way: `properties: {}`,
        `additionalProperties: false`, empty `required`."""
        param = InputParameter(
            name="items",
            required=True,
            description="Empty items.",
            value_schema=ValueSchema(
                val_type="array",
                inner_val_type="json",
                inner_properties={},
                inner_required_keys=[],
            ),
        )

        items = _convert_input_parameters_to_json_schema([param])["properties"]["items"]["items"]

        assert items["type"] == "object"
        assert items["properties"] == {}
        assert items["additionalProperties"] is False
        assert items["required"] == []


class TestOpenAIEndToEnd:
    """Catalog-to-converter checks for structured params."""

    def _to_openai_first_param(self, func, param_name):
        from arcade_core.catalog import ToolCatalog

        catalog = ToolCatalog()
        catalog.add_tool(func, "endtoend")
        mat_tool = next(iter(catalog))
        return to_openai(mat_tool)["function"]["parameters"]["properties"][param_name]

    def test_all_defaulted_pydantic_model_fields_not_forced(self):
        """A Pydantic model whose fields all have defaults must not force a value for any field:
        each field is rendered nullable even though its Python type is not `T | None`."""
        from arcade_tdk import tool
        from pydantic import BaseModel

        class AllDefaulted(BaseModel):
            count: int = 5
            label: str = "x"

        @tool(desc="t")
        def f(cfg: Annotated[AllDefaulted, "cfg"]) -> str:
            return ""

        schema = self._to_openai_first_param(f, "cfg")

        assert schema["additionalProperties"] is False
        assert set(schema["required"]) == {"count", "label"}
        assert schema["properties"]["count"]["type"] == ["integer", "null"]
        assert schema["properties"]["label"]["type"] == ["string", "null"]

    def test_freeform_dict_param_stays_open_end_to_end(self):
        """A `dict` param has an unknown shape and must stay an open object."""
        from arcade_tdk import tool

        @tool(desc="t")
        def f(payload: Annotated[dict, "payload"]) -> str:
            return ""

        schema = self._to_openai_first_param(f, "payload")

        assert schema["type"] == "object"
        assert "properties" not in schema
        assert "additionalProperties" not in schema

    def test_zero_field_pydantic_model_param_is_closed_end_to_end(self):
        """A zero-field Pydantic model has a known-empty shape and must close in strict mode."""
        from arcade_tdk import tool
        from pydantic import BaseModel

        class EmptyModel(BaseModel):
            pass

        @tool(desc="t")
        def f(cfg: Annotated[EmptyModel, "cfg"]) -> str:
            return ""

        schema = self._to_openai_first_param(f, "cfg")

        assert schema["type"] == "object"
        assert schema["properties"] == {}
        assert schema["additionalProperties"] is False
        assert schema["required"] == []

    def test_empty_typeddict_param_is_closed_end_to_end(self):
        """An empty TypedDict has a known-empty shape and must close in strict mode."""
        from arcade_tdk import tool
        from typing_extensions import TypedDict

        class EmptyTD(TypedDict):
            pass

        @tool(desc="t")
        def f(td: Annotated[EmptyTD, "td"]) -> str:
            return ""

        schema = self._to_openai_first_param(f, "td")

        assert schema["type"] == "object"
        assert schema["properties"] == {}
        assert schema["additionalProperties"] is False
        assert schema["required"] == []

    def test_list_of_empty_model_items_are_closed_end_to_end(self):
        """`list[<zero-field model>]` array items must close in strict mode."""
        from arcade_tdk import tool
        from pydantic import BaseModel

        class EmptyModel(BaseModel):
            pass

        @tool(desc="t")
        def f(items: Annotated[list[EmptyModel], "items"]) -> str:
            return ""

        items = self._to_openai_first_param(f, "items")["items"]

        assert items["type"] == "object"
        assert items["properties"] == {}
        assert items["additionalProperties"] is False
        assert items["required"] == []


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"]

    def test_array_of_object_items_expand_item_fields(self):
        """`list[<object>]` parameters expand the object's fields into the array `items`
        schema. OpenAI strict mode requires every field in `required` and
        `additionalProperties: false` on each item object; fields absent from
        `inner_required_keys` are expressed as nullable unions so they may be omitted."""
        param = InputParameter(
            name="attachments",
            required=False,
            description="Files to attach.",
            value_schema=ValueSchema(
                val_type="array",
                inner_val_type="json",
                inner_properties={
                    "source": ValueSchema(val_type="string"),
                    "filename": ValueSchema(val_type="string"),
                    "mime_type": ValueSchema(val_type="string"),
                },
                inner_required_keys=["source"],
            ),
        )

        items = _convert_input_parameters_to_json_schema([param])["properties"]["attachments"][
            "items"
        ]

        assert items.get("properties", {}).keys() == {"source", "filename", "mime_type"}
        assert items["additionalProperties"] is False
        assert set(items["required"]) == {"source", "filename", "mime_type"}
        assert items["properties"]["source"]["type"] == "string"
        assert items["properties"]["filename"]["type"] == ["string", "null"]
        assert items["properties"]["mime_type"]["type"] == ["string", "null"]

    def test_object_param_expands_with_strict_fields(self):
        """A top-level (and nested) object parameter is a full strict-mode object: every
        property in `required`, `additionalProperties: false`, and properties absent from
        `required_keys` expressed as nullable unions."""
        param = InputParameter(
            name="recipient",
            required=True,
            description="Message recipient.",
            value_schema=ValueSchema(
                val_type="json",
                properties={
                    "email": ValueSchema(val_type="string"),
                    "name": ValueSchema(val_type="string"),
                },
                required_keys=["email"],
            ),
        )

        schema = _convert_input_parameters_to_json_schema([param])["properties"]["recipient"]

        assert schema["type"] == "object"
        assert schema["additionalProperties"] is False
        assert set(schema["required"]) == {"email", "name"}
        assert schema["properties"]["email"]["type"] == "string"
        assert schema["properties"]["name"]["type"] == ["string", "null"]
