import asyncio
import inspect
import pickle
import re
import sys
from collections import UserDict
from datetime import datetime, timezone
from functools import partial
from typing import Annotated, Any, Generic, Literal, TypeVar, Union

import pytest
from pydantic_core import ArgsKwargs
from typing_extensions import Required, TypedDict, Unpack

from pydantic import (
    AfterValidator,
    AliasChoices,
    BaseModel,
    BeforeValidator,
    Field,
    PydanticInvalidForJsonSchema,
    PydanticUserError,
    Strict,
    TypeAdapter,
    ValidationError,
    validate_call,
    with_config,
)


def test_wrap() -> None:
    @validate_call
    def foo_bar(a: int, b: int):
        """This is the foo_bar method."""
        return f'{a}, {b}'

    assert foo_bar.__doc__ == 'This is the foo_bar method.'
    assert foo_bar.__name__ == 'foo_bar'
    assert foo_bar.__module__ == 'tests.test_validate_call'
    assert foo_bar.__qualname__ == 'test_wrap.<locals>.foo_bar'
    assert callable(foo_bar.raw_function)
    assert repr(inspect.signature(foo_bar)) == '<Signature (a: int, b: int)>'


def test_func_type() -> None:
    def f(x: int): ...

    class A:
        def m(self, x: int): ...

    for func in (f, lambda x: None, A.m, A().m):
        assert validate_call(func).__name__ == func.__name__
        assert validate_call(func).__qualname__ == func.__qualname__
        assert validate_call(partial(func)).__name__ == f'partial({func.__name__})'
        assert validate_call(partial(func)).__qualname__ == f'partial({func.__qualname__})'

    with pytest.raises(
        PydanticUserError,
        match=(f'Partial of `{list}` is invalid because the type of `{list}` is not supported by `validate_call`'),
    ):
        validate_call(partial(list))

    with pytest.raises(
        PydanticUserError,
        match=('`validate_call` should be applied to one of the following: function, method, partial, or lambda'),
    ):
        validate_call([])


def validate_bare_none() -> None:
    @validate_call
    def func(f: None):
        return f

    assert func(f=None) is None


def test_validate_class() -> None:
    class A:
        @validate_call
        def __new__(cls, x: int):
            return super().__new__(cls)

        @validate_call
        def __init__(self, x: int) -> None:
            self.x = x

    class M(type): ...

    for cls in (A, int, type, Exception, M):
        with pytest.raises(
            PydanticUserError,
            match=re.escape(
                '`validate_call` should be applied to functions, not classes (put `@validate_call` on top of `__init__` or `__new__` instead)'
            ),
        ):
            validate_call(cls)

    assert A('5').x == 5


def test_validate_custom_callable() -> None:
    class A:
        def __call__(self, x: int) -> int:
            return x

    with pytest.raises(
        PydanticUserError,
        match=re.escape(
            '`validate_call` should be applied to functions, not instances or other callables. Use `validate_call` explicitly on `__call__` instead.'
        ),
    ):
        validate_call(A())

    a = A()
    assert validate_call(a.__call__)('5') == 5  # Note: dunder methods cannot be overridden at instance level

    class B:
        @validate_call
        def __call__(self, x: int) -> int:
            return x

    assert B()('5') == 5


def test_invalid_signature() -> None:
    # Builtins functions not supported:
    with pytest.raises(PydanticUserError, match=(f'Input built-in function `{breakpoint}` is not supported')):
        validate_call(breakpoint)

    class A:
        def f(): ...

    # A method require at least one positional arg (i.e. `self`), so the signature is invalid
    func = A().f
    with pytest.raises(PydanticUserError, match=(f"Input function `{func}` doesn't have a valid signature")):
        validate_call(func)


@pytest.mark.parametrize('decorator', [staticmethod, classmethod])
def test_classmethod_order_error(decorator) -> None:
    name = decorator.__name__
    with pytest.raises(
        PydanticUserError,
        match=re.escape(f'The `@{name}` decorator should be applied after `@validate_call` (put `@{name}` on top)'),
    ):

        class A:
            @validate_call
            @decorator
            def method(self, x: int):
                pass


def test_args() -> None:
    @validate_call
    def foo(a: int, b: int):
        return f'{a}, {b}'

    assert foo(1, 2) == '1, 2'
    assert foo(*[1, 2]) == '1, 2'
    assert foo(*(1, 2)) == '1, 2'
    assert foo(*[1], 2) == '1, 2'
    assert foo(a=1, b=2) == '1, 2'
    assert foo(1, b=2) == '1, 2'
    assert foo(b=2, a=1) == '1, 2'

    with pytest.raises(ValidationError) as exc_info:
        foo()
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {'type': 'missing_argument', 'loc': ('a',), 'msg': 'Missing required argument', 'input': ArgsKwargs(())},
        {'type': 'missing_argument', 'loc': ('b',), 'msg': 'Missing required argument', 'input': ArgsKwargs(())},
    ]

    with pytest.raises(ValidationError) as exc_info:
        foo(1, 'x')
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'int_parsing',
            'loc': (1,),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'input': 'x',
        }
    ]

    with pytest.raises(ValidationError, match=r'2\s+Unexpected positional argument'):
        foo(1, 2, 3)

    with pytest.raises(ValidationError, match=r'apple\s+Unexpected keyword argument'):
        foo(1, 2, apple=3)

    with pytest.raises(ValidationError, match=r'a\s+Got multiple values for argument'):
        foo(1, 2, a=3)

    with pytest.raises(ValidationError) as exc_info:
        foo(1, 2, a=3, b=4)
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {'type': 'multiple_argument_values', 'loc': ('a',), 'msg': 'Got multiple values for argument', 'input': 3},
        {'type': 'multiple_argument_values', 'loc': ('b',), 'msg': 'Got multiple values for argument', 'input': 4},
    ]


def test_optional():
    @validate_call
    def foo_bar(a: int = None):
        return f'a={a}'

    assert foo_bar() == 'a=None'
    assert foo_bar(1) == 'a=1'
    with pytest.raises(ValidationError) as exc_info:
        foo_bar(None)

    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {'type': 'int_type', 'loc': (0,), 'msg': 'Input should be a valid integer', 'input': None}
    ]


def test_kwargs():
    @validate_call
    def foo(*, a: int, b: int):
        return a + b

    assert foo(a=1, b=3) == 4

    with pytest.raises(ValidationError) as exc_info:
        foo(a=1, b='x')

    assert exc_info.value.errors(include_url=False) == [
        {
            'input': 'x',
            'loc': ('b',),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'type': 'int_parsing',
        }
    ]

    with pytest.raises(ValidationError) as exc_info:
        foo(1, 'x')
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'missing_keyword_only_argument',
            'loc': ('a',),
            'msg': 'Missing required keyword only argument',
            'input': ArgsKwargs((1, 'x')),
        },
        {
            'type': 'missing_keyword_only_argument',
            'loc': ('b',),
            'msg': 'Missing required keyword only argument',
            'input': ArgsKwargs((1, 'x')),
        },
        {'type': 'unexpected_positional_argument', 'loc': (0,), 'msg': 'Unexpected positional argument', 'input': 1},
        {'type': 'unexpected_positional_argument', 'loc': (1,), 'msg': 'Unexpected positional argument', 'input': 'x'},
    ]


def test_untyped():
    @validate_call
    def foo(a, b, c='x', *, d='y'):
        return ', '.join(str(arg) for arg in [a, b, c, d])

    assert foo(1, 2) == '1, 2, x, y'
    assert foo(1, {'x': 2}, c='3', d='4') == "1, {'x': 2}, 3, 4"


@pytest.mark.parametrize('validated', (True, False))
def test_var_args_kwargs(validated):
    def foo(a, b, *args, d=3, **kwargs):
        return f'a={a!r}, b={b!r}, args={args!r}, d={d!r}, kwargs={kwargs!r}'

    if validated:
        foo = validate_call(foo)

    assert foo(1, 2) == 'a=1, b=2, args=(), d=3, kwargs={}'
    assert foo(1, 2, 3, d=4) == 'a=1, b=2, args=(3,), d=4, kwargs={}'
    assert foo(*[1, 2, 3], d=4) == 'a=1, b=2, args=(3,), d=4, kwargs={}'
    assert foo(1, 2, args=(10, 11)) == "a=1, b=2, args=(), d=3, kwargs={'args': (10, 11)}"
    assert foo(1, 2, 3, args=(10, 11)) == "a=1, b=2, args=(3,), d=3, kwargs={'args': (10, 11)}"
    assert foo(1, 2, 3, e=10) == "a=1, b=2, args=(3,), d=3, kwargs={'e': 10}"
    assert foo(1, 2, kwargs=4) == "a=1, b=2, args=(), d=3, kwargs={'kwargs': 4}"
    assert foo(1, 2, kwargs=4, e=5) == "a=1, b=2, args=(), d=3, kwargs={'kwargs': 4, 'e': 5}"


def test_unpacked_typed_dict_kwargs_invalid_type() -> None:
    with pytest.raises(PydanticUserError) as exc:

        @validate_call
        def foo(**kwargs: Unpack[int]):
            pass

    assert exc.value.code == 'unpack-typed-dict'


def test_unpacked_typed_dict_kwargs_overlaps() -> None:
    class TD(TypedDict, total=False):
        a: int
        b: int
        c: int

    with pytest.raises(PydanticUserError) as exc:

        @validate_call
        def foo(a: int, b: int, **kwargs: Unpack[TD]):
            pass

    assert exc.value.code == 'overlapping-unpack-typed-dict'
    assert exc.value.message == "Typed dictionary 'TD' overlaps with parameters 'a', 'b'"

    # Works for a pos-only argument
    @validate_call
    def foo(a: int, /, **kwargs: Unpack[TD]):
        pass

    foo(1, a=1)


def test_unpacked_typed_dict_kwargs() -> None:
    @with_config({'strict': True})
    class TD(TypedDict, total=False):
        a: int
        b: Required[str]

    @validate_call
    def foo1(**kwargs: Unpack[TD]):
        pass

    @validate_call
    def foo2(**kwargs: 'Unpack[TD]'):
        pass

    for foo in (foo1, foo2):
        foo(a=1, b='test')
        foo(b='test')

        with pytest.raises(ValidationError) as exc:
            foo(a='1')

        assert exc.value.errors()[0]['type'] == 'int_type'
        assert exc.value.errors()[0]['loc'] == ('a',)
        assert exc.value.errors()[1]['type'] == 'missing'
        assert exc.value.errors()[1]['loc'] == ('b',)

        # Make sure that when called without any arguments,
        # empty kwargs are still validated against the typed dict:
        with pytest.raises(ValidationError) as exc:
            foo()

        assert exc.value.errors()[0]['type'] == 'missing'
        assert exc.value.errors()[0]['loc'] == ('b',)


def test_unpacked_generic_typed_dict_kwargs() -> None:
    T = TypeVar('T')

    class TD(TypedDict, Generic[T]):
        t: T

    @validate_call
    def foo(**kwargs: Unpack[TD[int]]):
        pass

    with pytest.raises(ValidationError):
        foo(t='not_an_int')


def test_unpacked_typed_dict_kwargs_functional_syntax() -> None:
    TD = TypedDict('TD', {'in': int, 'x-y': int})

    @validate_call
    def foo(**kwargs: Unpack[TD]):
        pass

    foo(**{'in': 1, 'x-y': 2})

    with pytest.raises(ValidationError) as exc:
        foo(**{'in': 'not_an_int', 'x-y': 1})

    assert exc.value.errors()[0]['type'] == 'int_parsing'
    assert exc.value.errors()[0]['loc'] == ('in',)


def test_unpacked_typed_dict_kwargs_closed() -> None:
    class TD(TypedDict, closed=True):
        a: int

    @validate_call
    def foo(**kwargs: Unpack[TD]):
        pass

    foo(a=1)

    with pytest.raises(ValidationError) as exc:
        foo(a=1, b=2)

    assert exc.value.errors()[0]['type'] == 'extra_forbidden'
    assert exc.value.errors()[0]['loc'] == ('b',)


def test_unpacked_typed_dict_extra_items() -> None:
    class TD(TypedDict, extra_items=str):
        a: int

    @validate_call
    def foo(**kwargs: Unpack[TD]):
        return kwargs

    assert foo(a='1') == {'a': 1}
    assert foo(a=1, b='x', c='y') == {'a': 1, 'b': 'x', 'c': 'y'}

    with pytest.raises(ValidationError) as exc:
        foo(a=1, b=2)

    assert exc.value.errors()[0]['type'] == 'string_type'
    assert exc.value.errors()[0]['loc'] == ('b',)


def test_field_can_provide_factory() -> None:
    @validate_call
    def foo(a: int, b: int = Field(default_factory=lambda: 99), *args: int) -> int:
        """mypy is happy with this"""
        return a + b + sum(args)

    assert foo(3) == 102
    assert foo(1, 2, 3) == 6


def test_annotated_field_can_provide_factory() -> None:
    @validate_call
    def foo2(a: int, b: 'Annotated[int, Field(default_factory=lambda: 99)]', *args: int) -> int:
        """mypy reports Incompatible default for argument "b" if we don't supply ANY as default"""
        return a + b + sum(args)

    assert foo2(1) == 100


def test_positional_only(create_module):
    module = create_module(
        # language=Python
        """
from pydantic import validate_call

@validate_call
def foo(a, b, /, c=None):
    return f'{a}, {b}, {c}'
"""
    )
    assert module.foo(1, 2) == '1, 2, None'
    assert module.foo(1, 2, 44) == '1, 2, 44'
    assert module.foo(1, 2, c=44) == '1, 2, 44'
    with pytest.raises(ValidationError) as exc_info:
        module.foo(1, b=2)
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'missing_positional_only_argument',
            'loc': (1,),
            'msg': 'Missing required positional only argument',
            'input': ArgsKwargs((1,), {'b': 2}),
        },
        {'type': 'unexpected_keyword_argument', 'loc': ('b',), 'msg': 'Unexpected keyword argument', 'input': 2},
    ]

    with pytest.raises(ValidationError) as exc_info:
        module.foo(a=1, b=2)
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'missing_positional_only_argument',
            'loc': (0,),
            'msg': 'Missing required positional only argument',
            'input': ArgsKwargs((), {'a': 1, 'b': 2}),
        },
        {
            'type': 'missing_positional_only_argument',
            'loc': (1,),
            'msg': 'Missing required positional only argument',
            'input': ArgsKwargs((), {'a': 1, 'b': 2}),
        },
        {'type': 'unexpected_keyword_argument', 'loc': ('a',), 'msg': 'Unexpected keyword argument', 'input': 1},
        {'type': 'unexpected_keyword_argument', 'loc': ('b',), 'msg': 'Unexpected keyword argument', 'input': 2},
    ]


def test_args_name():
    @validate_call
    def foo(args: int, kwargs: int):
        return f'args={args!r}, kwargs={kwargs!r}'

    assert foo(1, 2) == 'args=1, kwargs=2'

    with pytest.raises(ValidationError, match=r'apple\s+Unexpected keyword argument'):
        foo(1, 2, apple=4)

    with pytest.raises(ValidationError) as exc_info:
        foo(1, 2, apple=4, banana=5)

    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {'type': 'unexpected_keyword_argument', 'loc': ('apple',), 'msg': 'Unexpected keyword argument', 'input': 4},
        {'type': 'unexpected_keyword_argument', 'loc': ('banana',), 'msg': 'Unexpected keyword argument', 'input': 5},
    ]

    with pytest.raises(ValidationError) as exc_info:
        foo(1, 2, 3)

    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {'type': 'unexpected_positional_argument', 'loc': (2,), 'msg': 'Unexpected positional argument', 'input': 3}
    ]


def test_async():
    @validate_call
    async def foo(a, b):
        return f'a={a} b={b}'

    async def run():
        v = await foo(1, 2)
        assert v == 'a=1 b=2'

    # insert_assert(inspect.iscoroutinefunction(foo) is True)
    assert inspect.iscoroutinefunction(foo) is True

    asyncio.run(run())
    with pytest.raises(ValidationError) as exc_info:
        asyncio.run(foo('x'))
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {'type': 'missing_argument', 'loc': ('b',), 'msg': 'Missing required argument', 'input': ArgsKwargs(('x',))}
    ]


def test_string_annotation():
    @validate_call
    def foo(a: 'list[int]', b: 'float'):
        return f'a={a!r} b={b!r}'

    assert foo([1, 2, 3], 22) == 'a=[1, 2, 3] b=22.0'

    with pytest.raises(ValidationError) as exc_info:
        foo(['x'])

    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'int_parsing',
            'loc': (0, 0),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'input': 'x',
        },
        {'type': 'missing_argument', 'loc': ('b',), 'msg': 'Missing required argument', 'input': ArgsKwargs((['x'],))},
    ]


def test_local_annotation():
    ListInt = list[int]

    @validate_call
    def foo(a: ListInt):
        return f'a={a!r}'

    assert foo([1, 2, 3]) == 'a=[1, 2, 3]'

    with pytest.raises(ValidationError) as exc_info:
        foo(['x'])

    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'int_parsing',
            'loc': (0, 0),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'input': 'x',
        },
    ]


def test_item_method():
    class X:
        def __init__(self, v):
            self.v = v

        @validate_call
        def foo(self, a: int, b: int):
            assert self.v == a
            return f'{a}, {b}'

    x = X(4)
    assert x.foo(4, 2) == '4, 2'
    assert x.foo(*[4, 2]) == '4, 2'

    with pytest.raises(ValidationError) as exc_info:
        x.foo()

    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {'type': 'missing_argument', 'loc': ('a',), 'msg': 'Missing required argument', 'input': ArgsKwargs((x,))},
        {'type': 'missing_argument', 'loc': ('b',), 'msg': 'Missing required argument', 'input': ArgsKwargs((x,))},
    ]


def test_class_method():
    class X:
        @classmethod
        @validate_call
        def foo(cls, a: int, b: int):
            assert cls == X
            return f'{a}, {b}'

    x = X()
    assert x.foo(4, 2) == '4, 2'
    assert x.foo(*[4, 2]) == '4, 2'

    with pytest.raises(ValidationError) as exc_info:
        x.foo()

    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {'type': 'missing_argument', 'loc': ('a',), 'msg': 'Missing required argument', 'input': ArgsKwargs((X,))},
        {'type': 'missing_argument', 'loc': ('b',), 'msg': 'Missing required argument', 'input': ArgsKwargs((X,))},
    ]


def test_json_schema():
    @validate_call
    def foo(a: int, b: int = None):
        return f'{a}, {b}'

    assert foo(1, 2) == '1, 2'
    assert foo(1, b=2) == '1, 2'
    assert foo(1) == '1, None'
    assert TypeAdapter(foo).json_schema() == {
        'type': 'object',
        'properties': {'a': {'title': 'A', 'type': 'integer'}, 'b': {'default': None, 'title': 'B', 'type': 'integer'}},
        'required': ['a'],
        'additionalProperties': False,
    }

    @validate_call
    def foo(a: int, /, b: int):
        return f'{a}, {b}'

    assert foo(1, 2) == '1, 2'
    assert TypeAdapter(foo).json_schema() == {
        'maxItems': 2,
        'minItems': 2,
        'prefixItems': [{'title': 'A', 'type': 'integer'}, {'title': 'B', 'type': 'integer'}],
        'type': 'array',
    }

    @validate_call
    def foo(a: int, /, *, b: int, c: int):
        return f'{a}, {b}, {c}'

    assert foo(1, b=2, c=3) == '1, 2, 3'
    with pytest.raises(
        PydanticInvalidForJsonSchema,
        match=(
            'Unable to generate JSON schema for arguments validator with positional-only and keyword-only arguments'
        ),
    ):
        TypeAdapter(foo).json_schema()

    @validate_call
    def foo(*numbers: int) -> int:
        return sum(numbers)

    assert foo(1, 2, 3) == 6
    assert TypeAdapter(foo).json_schema() == {'items': {'type': 'integer'}, 'type': 'array'}

    @validate_call
    def foo(a: int, *numbers: int) -> int:
        return a + sum(numbers)

    assert foo(1, 2, 3) == 6
    assert TypeAdapter(foo).json_schema() == {
        'items': {'type': 'integer'},
        'prefixItems': [{'title': 'A', 'type': 'integer'}],
        'minItems': 1,
        'type': 'array',
    }

    @validate_call
    def foo(**scores: int) -> str:
        return ', '.join(f'{k}={v}' for k, v in sorted(scores.items()))

    assert foo(a=1, b=2) == 'a=1, b=2'
    assert TypeAdapter(foo).json_schema() == {
        'additionalProperties': {'type': 'integer'},
        'properties': {},
        'type': 'object',
    }

    @validate_call
    def foo(a: Annotated[int, Field(alias='A')]):
        return a

    assert foo(1) == 1
    assert TypeAdapter(foo).json_schema() == {
        'additionalProperties': False,
        'properties': {'A': {'title': 'A', 'type': 'integer'}},
        'required': ['A'],
        'type': 'object',
    }


def test_json_schema_custom_title() -> None:
    def func(a: int):
        pass

    ta = TypeAdapter(func, config={'field_title_generator': lambda f_name, _: f_name + 'test'})

    assert ta.json_schema()['properties']['a']['title'] == 'atest'


def test_json_schema_title_not_set_on_ref() -> None:
    class Model(BaseModel):
        pass

    def func(m: Model):
        pass

    ta = TypeAdapter(func)

    assert ta.json_schema()['properties']['m'] == {'$ref': '#/$defs/Model'}


def test_alias_generator():
    @validate_call(config=dict(alias_generator=lambda x: x * 2))
    def foo(a: int, b: int):
        return f'{a}, {b}'

    assert foo(1, 2) == '1, 2'
    assert foo(aa=1, bb=2) == '1, 2'


def test_config_arbitrary_types_allowed():
    class EggBox:
        def __str__(self) -> str:
            return 'EggBox()'

    @validate_call(config=dict(arbitrary_types_allowed=True))
    def foo(a: int, b: EggBox):
        return f'{a}, {b}'

    assert foo(1, EggBox()) == '1, EggBox()'
    with pytest.raises(ValidationError) as exc_info:
        assert foo(1, 2) == '1, 2'

    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'is_instance_of',
            'loc': (1,),
            'msg': 'Input should be an instance of test_config_arbitrary_types_allowed.<locals>.EggBox',
            'input': 2,
            'ctx': {'class': 'test_config_arbitrary_types_allowed.<locals>.EggBox'},
        }
    ]


def test_config_strict():
    @validate_call(config=dict(strict=True))
    def foo(a: int, b: list[str]):
        return f'{a}, {b[0]}'

    assert foo(1, ['bar', 'foobar']) == '1, bar'
    with pytest.raises(ValidationError) as exc_info:
        foo('foo', ('bar', 'foobar'))
    assert exc_info.value.errors(include_url=False) == [
        {'type': 'int_type', 'loc': (0,), 'msg': 'Input should be a valid integer', 'input': 'foo'},
        {'type': 'list_type', 'loc': (1,), 'msg': 'Input should be a valid list', 'input': ('bar', 'foobar')},
    ]


def test_annotated_num():
    @validate_call
    def f(a: Annotated[int, Field(gt=0), Field(lt=10)]):
        return a

    assert f(5) == 5

    with pytest.raises(ValidationError) as exc_info:
        f(0)
    assert exc_info.value.errors(include_url=False) == [
        {'type': 'greater_than', 'loc': (0,), 'msg': 'Input should be greater than 0', 'input': 0, 'ctx': {'gt': 0}}
    ]

    with pytest.raises(ValidationError) as exc_info:
        f(10)
    assert exc_info.value.errors(include_url=False) == [
        {'type': 'less_than', 'loc': (0,), 'msg': 'Input should be less than 10', 'input': 10, 'ctx': {'lt': 10}}
    ]


def test_annotated_discriminator():
    class Cat(BaseModel):
        type: Literal['cat'] = 'cat'
        food: str
        meow: int

    class Dog(BaseModel):
        type: Literal['dog'] = 'dog'
        food: str
        bark: int

    Pet = Annotated[Union[Cat, Dog], Field(discriminator='type')]

    @validate_call
    def f(pet: Pet):
        return pet

    with pytest.raises(ValidationError) as exc_info:
        f({'food': 'fish'})

    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'union_tag_not_found',
            'loc': (0,),
            'msg': "Unable to extract tag using discriminator 'type'",
            'input': {'food': 'fish'},
            'ctx': {'discriminator': "'type'"},
        }
    ]

    with pytest.raises(ValidationError) as exc_info:
        f({'type': 'dog', 'food': 'fish'})

    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'missing',
            'loc': (0, 'dog', 'bark'),
            'msg': 'Field required',
            'input': {'type': 'dog', 'food': 'fish'},
        }
    ]


def test_annotated_validator():
    @validate_call
    def f(x: Annotated[int, BeforeValidator(lambda x: x + '2'), AfterValidator(lambda x: x + 1)]):
        return x

    assert f('1') == 13


def test_annotated_strict():
    @validate_call
    def f1(x: Annotated[int, Strict()]):
        return x

    @validate_call
    def f2(x: 'Annotated[int, Strict()]'):
        return x

    for f in (f1, f2):
        assert f(1) == 1

        with pytest.raises(ValidationError) as exc_info:
            f('1')

        assert exc_info.value.errors(include_url=False) == [
            {'type': 'int_type', 'loc': (0,), 'msg': 'Input should be a valid integer', 'input': '1'}
        ]


def test_annotated_use_of_alias():
    @validate_call
    def foo(a: Annotated[int, Field(alias='b')], c: Annotated[int, Field()], d: Annotated[int, Field(alias='')]):
        return a + c + d

    assert foo(**{'b': 10, 'c': 12, '': 1}) == 23

    with pytest.raises(ValidationError) as exc_info:
        assert foo(a=10, c=12, d=1) == 10

    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'missing_argument',
            'loc': ('b',),
            'msg': 'Missing required argument',
            'input': ArgsKwargs((), {'a': 10, 'c': 12, 'd': 1}),
        },
        {
            'type': 'missing_argument',
            'loc': ('',),
            'msg': 'Missing required argument',
            'input': ArgsKwargs((), {'a': 10, 'c': 12, 'd': 1}),
        },
        {'type': 'unexpected_keyword_argument', 'loc': ('a',), 'msg': 'Unexpected keyword argument', 'input': 10},
        {'type': 'unexpected_keyword_argument', 'loc': ('d',), 'msg': 'Unexpected keyword argument', 'input': 1},
    ]


def test_validation_alias():
    @validate_call
    def foo(
        a: Annotated[int, Field(validation_alias='b')],
        c: Annotated[int, Field(validation_alias=AliasChoices('d', 'e'))],
    ):
        return a + c

    assert foo(b=1, e=4) == 5


def test_use_of_alias():
    @validate_call
    def foo(c: int = Field(default_factory=lambda: 20), a: int = Field(default_factory=lambda: 10, alias='b')):
        return a + c

    assert foo(b=10) == 30


def test_validate_by_name():
    @validate_call(config=dict(validate_by_name=True))
    def foo(a: Annotated[int, Field(alias='b')], c: Annotated[int, Field(alias='d')]):
        return a + c

    assert foo(b=10, d=1) == 11
    assert foo(a=10, d=1) == 11
    assert foo(b=10, c=1) == 11
    assert foo(a=10, c=1) == 11


def test_validate_return():
    @validate_call(config=dict(validate_return=True))
    def foo(a: int, b: int) -> int:
        return a + b

    assert foo(1, 2) == 3


def test_validate_all():
    @validate_call(config=dict(validate_default=True))
    def foo(dt: datetime = Field(default_factory=lambda: 946684800)):
        return dt

    assert foo() == datetime(2000, 1, 1, tzinfo=timezone.utc)
    assert foo(0) == datetime(1970, 1, 1, tzinfo=timezone.utc)


def test_validate_all_positional(create_module):
    module = create_module(
        # language=Python
        """
from datetime import datetime

from pydantic import Field, validate_call

@validate_call(config=dict(validate_default=True))
def foo(dt: datetime = Field(default_factory=lambda: 946684800), /):
    return dt
"""
    )
    assert module.foo() == datetime(2000, 1, 1, tzinfo=timezone.utc)
    assert module.foo(0) == datetime(1970, 1, 1, tzinfo=timezone.utc)


def test_partial():
    def my_wrapped_function(a: int, b: int, c: int):
        return a + b + c

    my_partial_function = partial(my_wrapped_function, c=3)
    f = validate_call(my_partial_function)
    assert f(1, 2) == 6


def test_validator_init():
    class Foo:
        @validate_call
        def __init__(self, a: int, b: int):
            self.v = a + b

    assert Foo(1, 2).v == 3
    assert Foo(1, '2').v == 3
    with pytest.raises(ValidationError, match="type=int_parsing, input_value='x', input_type=str"):
        Foo(1, 'x')


def test_positional_and_keyword_with_same_name(create_module):
    module = create_module(
        # language=Python
        """
from pydantic import validate_call

@validate_call
def f(a: int, /, **kwargs):
    return a, kwargs
"""
    )
    assert module.f(1, a=2) == (1, {'a': 2})


def test_model_as_arg() -> None:
    class Model1(TypedDict):
        x: int

    class Model2(BaseModel):
        y: int

    @validate_call(validate_return=True)
    def f1(m1: Model1, m2: Model2) -> tuple[Model1, Model2]:
        return (m1, m2.model_dump())  # type: ignore

    res = f1({'x': '1'}, {'y': '2'})  # type: ignore
    assert res == ({'x': 1}, Model2(y=2))


def test_do_not_call_repr_on_validate_call() -> None:
    class Class:
        @validate_call
        def __init__(self, number: int) -> None: ...

        def __repr__(self) -> str:
            assert False

    Class(50)


def test_methods_are_not_rebound():
    class Thing:
        def __init__(self, x: int):
            self.x = x

        def a(self, x: int):
            return x + self.x

        c = validate_call(a)

    thing = Thing(1)
    assert thing.a == thing.a
    assert thing.c == thing.c
    assert Thing.c == Thing.c

    # Ensure validation is still happening
    assert Thing.c(thing, '2') == 3
    assert Thing(2).c('3') == 5


def test_basemodel_method():
    class Foo(BaseModel):
        @classmethod
        @validate_call
        def test(cls, x: int):
            return cls, x

    assert Foo.test('1') == (Foo, 1)

    class Bar(BaseModel):
        @validate_call
        def test(self, x: int):
            return self, x

    bar = Bar()
    assert bar.test('1') == (bar, 1)


def test_dynamic_method_decoration():
    class Foo:
        def bar(self, value: str) -> str:
            return f'bar-{value}'

    Foo.bar = validate_call(Foo.bar)
    assert Foo.bar

    foo = Foo()
    assert foo.bar('test') == 'bar-test'


def test_async_func() -> None:
    @validate_call(validate_return=True)
    async def foo(a: Any) -> int:
        return a

    res = asyncio.run(foo(1))
    assert res == 1

    with pytest.raises(ValidationError) as exc_info:
        asyncio.run(foo('x'))

    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'int_parsing',
            'loc': (),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'input': 'x',
        }
    ]


def test_validate_call_with_slots() -> None:
    class ClassWithSlots:
        __slots__ = {}

        @validate_call(validate_return=True)
        def some_instance_method(self, x: str) -> str:
            return x

        @classmethod
        @validate_call(validate_return=True)
        def some_class_method(cls, x: str) -> str:
            return x

        @staticmethod
        @validate_call(validate_return=True)
        def some_static_method(x: str) -> str:
            return x

    c = ClassWithSlots()
    assert c.some_instance_method(x='potato') == 'potato'
    assert c.some_class_method(x='pepper') == 'pepper'
    assert c.some_static_method(x='onion') == 'onion'

    # verify that equality still holds for instance methods
    assert c.some_instance_method == c.some_instance_method
    assert c.some_class_method == c.some_class_method
    assert c.some_static_method == c.some_static_method


def test_eval_type_backport():
    @validate_call
    def foo(bar: 'list[int | str]') -> 'list[int | str]':
        return bar

    assert foo([1, '2']) == [1, '2']
    with pytest.raises(ValidationError) as exc_info:
        foo('not a list')  # type: ignore
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'list_type',
            'loc': (0,),
            'msg': 'Input should be a valid list',
            'input': 'not a list',
        }
    ]
    with pytest.raises(ValidationError) as exc_info:
        foo([{'not a str or int'}])  # type: ignore
    # insert_assert(exc_info.value.errors(include_url=False))
    assert exc_info.value.errors(include_url=False) == [
        {
            'type': 'int_type',
            'loc': (0, 0, 'int'),
            'msg': 'Input should be a valid integer',
            'input': {'not a str or int'},
        },
        {
            'type': 'string_type',
            'loc': (0, 0, 'str'),
            'msg': 'Input should be a valid string',
            'input': {'not a str or int'},
        },
    ]


def test_eval_namespace_basic(create_module):
    module = create_module(
        """
from __future__ import annotations
from typing import TypeVar
from pydantic import validate_call

T = TypeVar('T', bound=int)

@validate_call
def f(x: T): ...

def g():
    MyList = list

    @validate_call
    def h(x: MyList[int]): ...
    return h
"""
    )
    f = module.f
    f(1)
    with pytest.raises(ValidationError) as exc_info:
        f('x')
    assert exc_info.value.errors(include_url=False) == [
        {
            'input': 'x',
            'loc': (0,),
            'msg': 'Input should be a valid integer, unable to parse string as an integer',
            'type': 'int_parsing',
        }
    ]

    h = module.g()
    with pytest.raises(ValidationError) as exc_info:
        h('not a list')
    assert exc_info.value.errors(include_url=False) == [
        {'input': 'not a list', 'loc': (0,), 'msg': 'Input should be a valid list', 'type': 'list_type'}
    ]


@pytest.mark.skipif(sys.version_info < (3, 12), reason='requires Python 3.12+ for PEP 695 syntax with generics')
def test_validate_call_with_pep_695_syntax(create_module) -> None:
    """Note: validate_call still doesn't work properly with generics, see https://github.com/pydantic/pydantic/issues/7796.

    This test is just to ensure that the syntax is accepted and doesn't raise a NameError."""

    module = create_module(
        """
from typing import Iterable
from pydantic import validate_call

@validate_call
def find_max_no_validate_return[T](args: 'Iterable[T]') -> T:
    return sorted(args, reverse=True)[0]

@validate_call(validate_return=True)
def find_max_validate_return[T](args: 'Iterable[T]') -> T:
    return sorted(args, reverse=True)[0]
        """
    )

    functions = [module.find_max_no_validate_return, module.find_max_validate_return]
    for find_max in functions:
        assert len(find_max.__type_params__) == 1
        assert find_max([1, 2, 10, 5]) == 10

        with pytest.raises(ValidationError):
            find_max(1)


@pytest.mark.skipif(sys.version_info < (3, 12), reason='requires Python 3.12+ for PEP 695 syntax with generics')
def test_pep695_with_class(create_module):
    """Primarily to ensure that the syntax is accepted and doesn't raise a `NameError` with `T`.
    The validation is not expected to work properly when parameterized at this point."""

    for import_annotations in ('from __future__ import annotations', ''):
        module = create_module(
            f"""
{import_annotations}
from pydantic import validate_call

class A[T]:
    @validate_call(validate_return=True)
    def f(self, a: T) -> T:
        return str(a)
            """
        )
        A = module.A
        a = A[int]()
        # these two are undesired behavior, but it's what happens now
        assert a.f(1) == '1'
        assert a.f('1') == '1'


@pytest.mark.skipif(sys.version_info < (3, 12), reason='requires Python 3.12+ for PEP 695 syntax with generics')
def test_pep695_with_nested_scopes(create_module):
    """Nested scopes generally cannot be caught by `parent_frame_namespace`,
    so currently this test is expected to fail.
    """

    module = create_module(
        """
from __future__ import annotations
from pydantic import validate_call

class A[T]:
    def g(self):
        @validate_call(validate_return=True)
        def inner(a: T) -> T: ...

    def h[S](self):
        @validate_call(validate_return=True)
        def inner(a: T) -> S: ...
        """
    )

    A = module.A
    a = A[int]()
    with pytest.raises(NameError):
        a.g()
    with pytest.raises(NameError):
        a.h()

    with pytest.raises(NameError):
        create_module(
            """
from __future__ import annotations
from pydantic import validate_call

class A[T]:
    class B:
        @validate_call(validate_return=True)
        def f(a: T) -> T: ...

    class C[S]:
        @validate_call(validate_return=True)
        def f(a: T) -> S: ...
            """
        )


class M0(BaseModel):
    z: int


M = M0


def test_uses_local_ns():
    class M1(BaseModel):
        y: int

    M = M1  # noqa: F841

    def foo():
        class M2(BaseModel):
            z: int

        M = M2  # noqa: F841

        @validate_call(validate_return=True)
        def bar(m: 'M') -> 'M':
            return m

        assert bar({'z': 1}) == M2(z=1)

    foo()


# The class needs to be defined at the module level
# For 'DeferBuildClass' to resolve:
class DeferBuildClass(BaseModel):
    @classmethod
    @validate_call(config={'defer_build': True})
    def cls_meth(cls, x: int) -> 'DeferBuildClass':
        return DeferBuildClass()


def test_validate_call_defer_build() -> None:
    DeferBuildClass.cls_meth(x=1)

    with pytest.raises(ValidationError):
        DeferBuildClass.cls_meth(x='not_an_int')


class _PickleTestModel(BaseModel):
    number: float


class _PickleTestDict(UserDict[int, _PickleTestModel]):
    @validate_call
    def __setitem__(self, key: int, value: _PickleTestModel):
        super().__setitem__(key, value)


def test_pickle_validate_call_with_basemodel() -> None:
    """https://github.com/pydantic/pydantic/issues/7846."""
    my_dict = _PickleTestDict({1: _PickleTestModel(number=1.0)})

    unpickled = pickle.loads(pickle.dumps(my_dict))

    assert unpickled == {1: _PickleTestModel(number=1.0)}
    assert unpickled[1].number == 1.0
