import sys
import typing

import pytest

from needle import tool, Field
from needle.agent.tools import build_schema, pydantic_schema, _is_pydantic_model

pep604 = pytest.mark.skipif(sys.version_info < (3, 10),
                            reason="PEP 604 unions need Python 3.10+")


def test_basic_schema_types_and_required():
    @tool
    def add(a: int, b: int) -> int:
        "Add two numbers."
        return a + b

    schema = add._needle_tool
    assert schema["name"] == "add"
    assert schema["description"] == "Add two numbers."
    assert schema["parameters"]["properties"]["a"] == {"type": "integer"}
    assert set(schema["parameters"]["required"]) == {"a", "b"}


def test_defaults_are_optional():
    def f(city: str, units: str = "metric"):
        pass

    schema = build_schema(f)
    assert schema["parameters"]["required"] == ["city"]
    assert "units" in schema["parameters"]["properties"]


def test_literal_list_and_dict_types():
    def f(mode: typing.Literal["fast", "slow"], items: list, meta: dict):
        pass

    props = build_schema(f)["parameters"]["properties"]
    assert props["mode"] == {"type": "string", "enum": ["fast", "slow"]}
    assert props["items"]["type"] == "array"
    assert props["meta"] == {"type": "object"}


def test_typed_list_items():
    def f(tags: typing.List[int]):
        pass

    props = build_schema(f)["parameters"]["properties"]
    assert props["tags"] == {"type": "array", "items": {"type": "integer"}}


def test_optional_annotation_not_required():
    def f(a: str, b: typing.Optional[int] = None):
        pass

    schema = build_schema(f)
    assert schema["parameters"]["required"] == ["a"]


@pep604
def test_pep604_none_union_not_required():
    def f(a: str, b: str | None = None, c: int | None = None, d: str | int | None = None):
        pass

    schema = build_schema(f)
    assert schema["parameters"]["required"] == ["a"]
    props = schema["parameters"]["properties"]
    assert props["b"] == {"type": "string"}
    assert props["c"] == {"type": "integer"}
    assert props["d"] == {"type": "string"}


@pep604
def test_pep604_union_matches_typing_union():
    def f(value: int | str):
        pass

    def g(value: typing.Union[int, str]):
        pass

    assert build_schema(f)["parameters"] == build_schema(g)["parameters"]
    assert build_schema(f)["parameters"]["properties"]["value"] == {"type": "integer"}


def test_field_constraints_and_docstring_args():
    def f(temp: int = Field(description="temperature", ge=0, le=100),
          name: str = Field(default="x", pattern="^[a-z]+$", min_length=1)):
        """Set the thing.

        Args:
            temp: the temperature in C
        """

    schema = build_schema(f)
    props = schema["parameters"]["properties"]
    assert props["temp"]["minimum"] == 0 and props["temp"]["maximum"] == 100
    assert props["temp"]["description"] == "temperature"
    assert props["name"]["pattern"] == "^[a-z]+$"
    assert props["name"]["minLength"] == 1
    assert "temp" in schema["parameters"]["required"]
    assert "name" not in schema["parameters"].get("required", [])


def test_docstring_description_falls_back_to_args():
    def f(city: str):
        """Look up weather.

        Args:
            city: the city to look up
        """

    props = build_schema(f)["parameters"]["properties"]
    assert props["city"]["description"] == "the city to look up"


def test_pydantic_model_schema():
    import pydantic

    class Weather(pydantic.BaseModel):
        "Weather query."
        city: str
        units: str = "metric"

    assert _is_pydantic_model(Weather)
    schema = pydantic_schema(Weather)
    assert schema["name"] == "Weather"
    assert "city" in schema["parameters"]["properties"]
    assert schema["parameters"]["required"] == ["city"]
    assert schema["description"] == "Weather query."


def test_tool_decorator_preserves_callable():
    @tool
    def greet(name: str) -> str:
        "Greet someone."
        return "hi " + name

    assert greet("bob") == "hi bob"
    assert greet._needle_tool["name"] == "greet"