<reponame>Finistere/dependency_manager<filename>tests/core/test_injection.py
import functools
import itertools
from typing import Iterator, Optional, Sequence, Union

import pytest
from typing_extensions import Annotated

from antidote import From, FromArg, Get, world
from antidote.core.annotations import Provide
from antidote.core.exceptions import NoInjectionsFoundError
from antidote.core.injection import inject, validate_injection
from antidote.exceptions import DependencyNotFoundError, DoubleInjectionError


class MyService:
    pass


class AnotherService:
    pass


@pytest.fixture(autouse=True)
def setup_world() -> Iterator[None]:
    with world.test.empty():
        world.test.singleton({MyService: MyService(), AnotherService: AnotherService()})
        yield


SENTINEL = "SENTINEL"


class DoesNotRaise:
    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass


@pytest.mark.parametrize(
    "kwargs, expectation",
    [
        pytest.param(kwargs, expectation, id=str(kwargs))
        for kwargs, expectation in [
            (dict(), DoesNotRaise()),
            (dict(dependencies=lambda arg: arg.name), DoesNotRaise()),
            (dict(dependencies=dict(x="x")), DoesNotRaise()),
            (dict(dependencies=["x"]), DoesNotRaise()),
            (dict(auto_provide=True), DoesNotRaise()),
            (
                dict(dependencies="{arg_name}"),
                pytest.raises(TypeError, match=".*dependencies.*str.*"),
            ),
            (dict(dependencies=1), pytest.raises(TypeError, match=".*dependencies.*int.*")),
            (dict(auto_provide=1), pytest.raises(TypeError, match=".*auto_provide.*int.*")),
            (dict(auto_provide=["x"]), pytest.raises(TypeError, match=".*auto_provide.*x.*")),
        ]
    ],
)
def test_validate_injection(kwargs, expectation):
    with expectation:

        @inject(**kwargs)
        def f(x: MyService):
            pass

    with expectation:
        validate_injection(**kwargs)


@pytest.fixture(params=[inject])
def injector(request):
    return request.param


def test_simple(injector):
    @injector
    def f(x: Provide[MyService]):
        return x

    with world.test.empty():
        s = MyService()
        world.test.singleton(MyService, s)
        assert s == f()


no_SENTINEL_injection = [
    pytest.param((None, None), dict(), id="nothing"),
    pytest.param(
        (MyService, None), dict(dependencies=dict(first=MyService)), id="dependencies:dict-first"
    ),
    pytest.param((MyService, None), dict(dependencies=(MyService,)), id="dependencies:tuple-first"),
    pytest.param(
        (None, MyService), dict(dependencies=dict(second=MyService)), id="dependencies:dict-second"
    ),
    pytest.param(
        (None, MyService), dict(dependencies=(None, MyService)), id="dependencies:tuple-second"
    ),
    pytest.param(
        ("first", "second"), dict(dependencies=lambda arg: arg.name), id="dependencies:callable"
    ),
    pytest.param(
        (MyService, MyService),
        dict(dependencies=lambda arg: MyService),
        id="dependencies:callable2",
    ),
    pytest.param((None, None), dict(dependencies=lambda s: None), id="dependencies:callable3"),
]


@pytest.mark.parametrize("expected,kwargs", no_SENTINEL_injection)
def test_without_type_hints(injector, expected, kwargs):
    @injector(**kwargs)
    def f(first=SENTINEL, second=SENTINEL):
        return first, second

    class A:
        @injector(**kwargs)
        def method(self, first=SENTINEL, second=SENTINEL):
            return first, second

        @injector(**kwargs)
        @classmethod
        def class_method(cls, first=SENTINEL, second=SENTINEL):
            return first, second

        @injector(**kwargs)
        @staticmethod
        def static_method(first=SENTINEL, second=SENTINEL):
            return first, second

        @classmethod
        @injector(**kwargs)
        def klass(cls, first=SENTINEL, second=SENTINEL):
            return first, second

    world.test.singleton(
        {"first": object(), "second": object(), "prefix:first": object(), "prefix:second": object()}
    )

    expected = tuple((world.get(d) if d is not None else SENTINEL for d in expected))
    (first, second) = expected
    a, b = object(), object()

    assert (a, second) == f(first=a)
    assert (a, second) == A().method(first=a)
    assert (a, second) == A.class_method(first=a)
    assert (a, second) == A.static_method(first=a)
    assert (a, second) == A.klass(first=a)

    assert (first, b) == f(second=b)
    assert (first, b) == A().method(second=b)
    assert (first, b) == A.class_method(second=b)
    assert (first, b) == A.static_method(second=b)
    assert (first, b) == A.klass(second=b)

    assert (a, second) == f(a)
    assert (a, second) == A().method(a)
    assert (a, second) == A.class_method(a)
    assert (a, second) == A.static_method(a)
    assert (a, second) == A.klass(a)

    assert (a, b) == f(a, b)
    assert (a, b) == A().method(a, b)
    assert (a, b) == A.class_method(a, b)
    assert (a, b) == A.static_method(a, b)
    assert (a, b) == A.klass(a, b)


@pytest.mark.parametrize(
    "expected,kwargs",
    no_SENTINEL_injection
    + [
        pytest.param((MyService, AnotherService), dict(auto_provide=True), id="auto_provide:True"),
        pytest.param((None, None), dict(auto_provide=False), id="auto_provide:False"),
        pytest.param(
            (MyService, None), dict(auto_provide=[MyService]), id="auto_provide:list-first"
        ),
        pytest.param(
            (None, AnotherService),
            dict(auto_provide=[AnotherService]),
            id="auto_provide:list-second",
        ),
        pytest.param((None, None), dict(auto_provide=[]), id="auto_provide:list-None"),
        pytest.param(
            (MyService, AnotherService),
            dict(auto_provide=lambda cls: True),
            id="auto_provide:func-True",
        ),
        pytest.param(
            (None, None), dict(auto_provide=lambda cls: False), id="auto_provide:func-False"
        ),
        pytest.param(
            (MyService, None),
            dict(auto_provide=lambda cls: issubclass(cls, MyService)),
            id="auto_provide:func-first",
        ),
        pytest.param(
            (None, AnotherService),
            dict(auto_provide=lambda cls: issubclass(cls, AnotherService)),
            id="auto_provide:func-second",
        ),
        pytest.param(
            (MyService, MyService),
            dict(dependencies=dict(second=MyService), auto_provide=True),
            id="auto_provide&dependencies:second",
        ),
        pytest.param(
            (None, MyService),
            dict(dependencies=dict(second=MyService), auto_provide=[AnotherService]),
            id="auto_provide&dependencies:second",
        ),
    ],
)
def test_with_auto_provide(injector, expected, kwargs):
    @injector(**kwargs)
    def f(first: MyService = SENTINEL, second: AnotherService = SENTINEL):
        return first, second

    class A:
        @injector(**kwargs)
        def method(self, first: MyService = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

        @injector(**kwargs)
        @classmethod
        def class_method(cls, first: MyService = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

        @injector(**kwargs)
        @staticmethod
        def static_method(first: MyService = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

        @classmethod
        @injector(**kwargs)
        def klass(cls, first: MyService = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

    world.test.singleton(
        {
            "first": "first",
            "second": "second",
            "prefix:first": "prefix:first",
            "prefix:second": "prefix:second",
        }
    )

    expected = tuple((world.get(d) if d is not None else SENTINEL for d in expected))
    (first, second) = expected
    a, b = "a", "b"

    assert (a, second) == f(first=a)
    assert (a, second) == A().method(first=a)
    assert (a, second) == A.class_method(first=a)
    assert (a, second) == A.static_method(first=a)
    assert (a, second) == A.klass(first=a)

    assert (first, b) == f(second=b)
    assert (first, b) == A().method(second=b)
    assert (first, b) == A.class_method(second=b)
    assert (first, b) == A.static_method(second=b)
    assert (first, b) == A.klass(second=b)

    assert (a, second) == f(a)
    assert (a, second) == A().method(a)
    assert (a, second) == A.class_method(a)
    assert (a, second) == A.static_method(a)
    assert (a, second) == A.klass(a)

    assert (a, b) == f(a, b)
    assert (a, b) == A().method(a, b)
    assert (a, b) == A.class_method(a, b)
    assert (a, b) == A.static_method(a, b)
    assert (a, b) == A.klass(a, b)


@pytest.mark.parametrize(
    "expected, kwargs",
    [
        pytest.param((MyService, None), dict(), id="nothing"),
        pytest.param(
            (MyService, None),
            dict(dependencies=dict(first=AnotherService)),
            id="dependencies:dict-first",
        ),
        pytest.param(
            (MyService, None), dict(dependencies=(AnotherService,)), id="dependencies:tuple-first"
        ),
        pytest.param(
            (MyService, MyService),
            dict(dependencies=dict(second=MyService)),
            id="dependencies:dict-second",
        ),
        pytest.param(
            (MyService, MyService),
            dict(dependencies=(None, MyService)),
            id="dependencies:tuple-second",
        ),
        pytest.param(
            (MyService, "second"),
            dict(dependencies=lambda arg: arg.name),
            id="dependencies:callable",
        ),
        pytest.param(
            (MyService, AnotherService),
            dict(dependencies=lambda arg: AnotherService),
            id="dependencies:callable2",
        ),
        pytest.param(
            (MyService, None), dict(dependencies=lambda arg: None), id="dependencies:callable3"
        ),
        pytest.param((MyService, AnotherService), dict(auto_provide=True), id="auto_provide:True"),
        pytest.param((MyService, None), dict(auto_provide=False), id="auto_provide:False"),
        pytest.param(
            (MyService, None), dict(auto_provide=[MyService]), id="auto_provide:list-first"
        ),
        pytest.param(
            (MyService, AnotherService),
            dict(auto_provide=[AnotherService]),
            id="auto_provide:list-second",
        ),
        pytest.param(
            (MyService, None),
            dict(auto_provide=lambda cls: issubclass(cls, MyService)),
            id="auto_provide:func-first",
        ),
        pytest.param(
            (MyService, AnotherService),
            dict(auto_provide=lambda cls: issubclass(cls, AnotherService)),
            id="auto_provide:func-second",
        ),
    ],
)
def test_with_provide(injector, expected, kwargs):
    @injector(**kwargs)
    def f(first: Provide[MyService] = SENTINEL, second: AnotherService = SENTINEL):
        return first, second

    class A:
        @injector(**kwargs)
        def method(self, first: Provide[MyService] = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

        @injector(**kwargs)
        @classmethod
        def class_method(
            cls, first: Provide[MyService] = SENTINEL, second: AnotherService = SENTINEL
        ):
            return first, second

        @injector(**kwargs)
        @staticmethod
        def static_method(first: Provide[MyService] = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

        @classmethod
        @injector(**kwargs)
        def klass(cls, first: Provide[MyService] = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

    world.test.singleton(
        {"first": object(), "second": object(), "prefix:first": object(), "prefix:second": object()}
    )

    expected = tuple((world.get(d) if d is not None else SENTINEL for d in expected))
    (first, second) = expected
    a, b = object(), object()

    assert (a, second) == f(first=a)
    assert (a, second) == A().method(first=a)
    assert (a, second) == A.class_method(first=a)
    assert (a, second) == A.static_method(first=a)
    assert (a, second) == A.klass(first=a)

    assert (first, b) == f(second=b)
    assert (first, b) == A().method(second=b)
    assert (first, b) == A.class_method(second=b)
    assert (first, b) == A.static_method(second=b)
    assert (first, b) == A.klass(second=b)

    assert (a, second) == f(a)
    assert (a, second) == A().method(a)
    assert (a, second) == A.class_method(a)
    assert (a, second) == A.static_method(a)
    assert (a, second) == A.klass(a)

    assert (a, b) == f(a, b)
    assert (a, b) == A().method(a, b)
    assert (a, b) == A.class_method(a, b)
    assert (a, b) == A.static_method(a, b)
    assert (a, b) == A.klass(a, b)


@pytest.mark.parametrize(
    "type_hint",
    # builtins
    [str, int, float, set, list, dict, complex, type, tuple, bytes, bytearray]
    # typing
    + [Optional, Sequence]
    # not a class / weird stuff
    + [1, lambda x: x, object()],
)
def test_ignored_type_hints(injector, type_hint):
    @injector(auto_provide=True)
    def f(x: type_hint):
        pass

    world.test.singleton(type_hint, object())
    with pytest.raises(TypeError):
        f()


def test_none_optional_support(injector):
    class Dummy:
        pass

    @injector
    def f(x: Provide[MyService] = None):
        return x

    @injector
    def g(x: Optional[Provide[MyService]] = None):
        return x

    with world.test.empty():
        s = MyService()
        world.test.singleton(Dummy, Dummy())
        world.test.singleton(MyService, s)
        assert f() is s
        assert g() is s

    with world.test.empty():
        world.test.singleton(Dummy, Dummy())
        assert f() is None
        assert g() is None


@pytest.mark.parametrize("auto_provide", [True, False])
def test_none_optional_support_auto_provide(injector, auto_provide):
    class Dummy:
        pass

    @injector(auto_provide=auto_provide)
    def f(x: MyService = None):
        return x

    @injector(auto_provide=auto_provide)
    def g(x: Optional[MyService] = None):
        return x

    @injector(auto_provide=auto_provide)
    def h(x: Union[MyService, Dummy]):
        pass

    with world.test.empty():
        s = MyService()
        world.test.singleton(Dummy, Dummy())
        world.test.singleton(MyService, s)
        expected = s if auto_provide else None
        assert f() is expected
        assert g() is expected
        with pytest.raises(TypeError):
            h()

    with world.test.empty():
        world.test.singleton(Dummy, Dummy())
        assert f() is None
        assert g() is None


def test_annotations(injector):
    class Dummy:
        pass

    with world.test.empty():

        @injector
        def custom_annotated(x: Annotated[Dummy, object()]):
            return x

        world.test.singleton(Dummy, Dummy())
        with pytest.raises(TypeError):
            custom_annotated()

    with world.test.empty():

        @injector
        def optional_annotated(x: Annotated[Dummy, object()] = None):
            return x

        world.test.singleton(Dummy, Dummy())
        assert optional_annotated() is None

    with world.test.empty():

        @injector
        def get(x: Annotated[Dummy, Get("dummy")]):
            return x

        world.test.singleton("dummy", Dummy())
        assert get() is world.get("dummy")

    with world.test.empty():

        @injector
        def optional_get(x: Optional[Annotated[Dummy, Get("dummy")]] = None):
            return x

        world.test.singleton("dummy", Dummy())
        assert optional_get() is world.get("dummy")

    with world.test.empty():

        @injector
        def from_arg(x: Annotated[Dummy, FromArg(lambda arg: arg.name)]):
            return x

        world.test.singleton("x", Dummy())
        assert from_arg() is world.get("x")

    with world.test.empty():

        class Maker:
            def __rmatmul__(self, other):
                return "dummy"

        @injector
        def get(x: Annotated[Dummy, From(Maker())]):
            return x

        world.test.singleton("dummy", Dummy())
        assert get() is world.get("dummy")


def test_multiple_antidote_annotations(injector):
    class Dummy:
        pass

    class Maker:
        def __rmatmul__(self, other):
            return "dummy"

    annotations = [
        Get("dummy"),
        From(Maker()),
        FromArg(lambda arg: arg.name),
    ]
    for (a, b) in itertools.combinations(annotations, 2):
        with pytest.raises(TypeError):

            @injector
            def custom_annotated(x: Annotated[Dummy, a, b]):
                return x


@pytest.fixture(params=["function", "method", "classmethod", "classmethod-after", "staticmethod"])
def injected_method_with(request, injector):
    kind = request.param

    def builder(**kwargs):
        if kind == "function":

            @injector(**kwargs)
            def f(x: MyService):
                return x

            return f
        else:

            class Dummy:
                @injector(**kwargs)
                def method(self, x: MyService):
                    return x

                @classmethod
                @injector(**kwargs)
                def class_method(cls, x: MyService):
                    return x

                @injector(**kwargs)
                @classmethod
                def class_method_after(cls, x: MyService):
                    return x

                @injector(**kwargs)
                @staticmethod
                def static_method(x: MyService):
                    return x

            if kind == "method":
                return Dummy().method
            elif kind == "classmethod":
                return Dummy.class_method
            elif kind == "classmethod-after":
                return Dummy.class_method_after
            elif kind == "staticmethod":
                return Dummy.static_method
            else:
                raise RuntimeError()

    return builder


@pytest.mark.parametrize(
    "expectation,kwargs",
    [
        pytest.param(
            pytest.raises(TypeError, match=".*dependencies.*"),
            dict(dependencies=object()),
            id="dependencies:unsupported-type",
        ),
        pytest.param(
            pytest.raises(TypeError),
            dict(dependencies={1: "x"}),
            id="dependencies:invalid-key-type",
        ),
        pytest.param(
            pytest.raises(TypeError, match=".*ignore_type_hints.*"),
            dict(ignore_type_hints=object()),
            id="ignore_type_hints:unsupported-type",
        ),
        pytest.param(
            pytest.raises(TypeError, match=".*auto_provide.*"),
            dict(auto_provide=object()),
            id="auto_provide:unsupported-type",
        ),
        pytest.param(
            pytest.raises(TypeError, match=".*strict_validation.*"),
            dict(strict_validation=object()),
            id="auto_provide:unsupported-type",
        ),
    ],
)
def test_invalid_args(injected_method_with, expectation, kwargs):
    with expectation:
        injected_method_with(**kwargs)()

    with expectation:
        validate_injection(**kwargs)


@pytest.mark.parametrize(
    "expectation,kwargs",
    [
        pytest.param(pytest.raises(TypeError), dict(), id="nothing"),
        pytest.param(
            pytest.raises(DependencyNotFoundError, match=".*Service.*"),
            dict(dependencies=(MyService,)),
            id="dependencies:tuple",
        ),
        pytest.param(
            pytest.raises(DependencyNotFoundError, match=".*Service.*"),
            dict(dependencies=dict(x=MyService)),
            id="dependencies:dict",
        ),
        pytest.param(
            pytest.raises(DependencyNotFoundError, match=".*Service.*"),
            dict(dependencies=lambda arg: MyService),
            id="dependencies:callable",
        ),
        pytest.param(pytest.raises(TypeError), dict(auto_provide=False), id="auto_provide:False"),
        pytest.param(
            pytest.raises(DependencyNotFoundError, match=".*Service.*"),
            dict(auto_provide=True),
            id="auto_provide:True",
        ),
        pytest.param(
            pytest.raises(DependencyNotFoundError, match=".*Service.*"),
            dict(auto_provide=[MyService]),
            id="auto_provide:list",
        ),
    ],
)
def test_unknown_dependency(injected_method_with, expectation, kwargs):
    with world.test.empty():
        with expectation:
            injected_method_with(**kwargs)()


def test_unknown_provide(injector):
    with world.test.empty():

        @injector
        def f(x: Provide[MyService]):
            return x

        with pytest.raises(DependencyNotFoundError):
            f()


@pytest.mark.parametrize(
    "expectation,kwargs",
    [
        pytest.param(
            pytest.raises(ValueError, match=".*y.*"),
            dict(auto_provide=[AnotherService]),
            id="auto_provide",
        ),
        pytest.param(
            pytest.raises(ValueError, match=".*y.*"),
            dict(auto_provide=[MyService, AnotherService]),
            id="auto_provide2",
        ),
        pytest.param(
            pytest.raises(ValueError),
            dict(dependencies=(None, None)),
            id="dependencies:too-much-arguments",
        ),
        pytest.param(
            pytest.raises(ValueError, match=".*unknown.*"),
            dict(dependencies=dict(unknown=DependencyNotFoundError)),
            id="dependencies:unknown-dict-key",
        ),
    ],
)
@pytest.mark.parametrize("strict", [True, False])
def test_strict_validation(injected_method_with, injector, expectation, kwargs, strict):
    kwargs["strict_validation"] = strict
    if strict:
        with expectation:
            injected_method_with(**kwargs)
    else:
        injected_method_with(**kwargs)(object())


@pytest.mark.parametrize(
    "expectation,kwargs",
    [
        pytest.param(
            pytest.raises(ValueError, match=".*self.*"),
            dict(dependencies=dict(self="x")),
            id="dependencies",
        ),
    ],
)
def test_cannot_inject_self(injector, expectation, kwargs):
    with expectation:

        class A:
            @injector(**kwargs)
            def method(self, x=None):
                return x

        A()

    with expectation:

        class A:
            @injector(**kwargs)
            @classmethod
            def classmethod(self, x=None):
                return x

        A()

    with expectation:

        class A:
            @classmethod
            @injector(**kwargs)
            def classmethod(self, x=None):
                return x

        A()


def test_no_injections():
    def f(x):
        return x

    # When nothing can be injected, the same function should be returned
    assert inject(f) is f


def test_double_injection(injector):
    # When the function has already its arguments injected, the same function should
    # be returned
    with pytest.raises(DoubleInjectionError, match=".*<locals>.f.*"):

        @injector
        @injector(auto_provide=True)
        def f(x: MyService):
            return x

    with pytest.raises(DoubleInjectionError, match=".*StaticmethodA.*"):

        class StaticmethodA:
            @injector
            @staticmethod
            @injector(auto_provide=True)
            def f(x, y: MyService):
                pass

    with pytest.raises(DoubleInjectionError, match=".*StaticmethodB.*"):

        class StaticmethodB:
            @injector
            @injector(auto_provide=True)
            @staticmethod
            def f(x, y: MyService):
                pass

    with pytest.raises(DoubleInjectionError, match=".*ClassmethodA.*"):

        class ClassmethodA:
            @injector
            @classmethod
            @injector(auto_provide=True)
            def f(cls, x: MyService):
                pass

    with pytest.raises(DoubleInjectionError, match=".*ClassmethodB.*"):

        class ClassmethodB:
            @injector
            @injector(auto_provide=True)
            @classmethod
            def f(cls, x: MyService):
                pass

    with pytest.raises(DoubleInjectionError, match=".*Method.*"):

        class Method:
            @injector
            @injector(auto_provide=True)
            def f(self, x: MyService):
                pass


def test_invalid_inject(injector):
    with pytest.raises(TypeError):

        @injector
        class Dummy:
            pass

    with pytest.raises(TypeError):
        injector(1)


def test_static_class_method(injector):
    class Dummy:
        @injector(auto_provide=True)
        @staticmethod
        def static(x: MyService):
            pass

        @injector(auto_provide=True)
        @classmethod
        def klass(cls, x: MyService):
            pass

    assert isinstance(Dummy.__dict__["static"], staticmethod)
    assert isinstance(Dummy.__dict__["klass"], classmethod)


@pytest.mark.asyncio
async def test_async(injector):
    service = world.get(MyService)
    another_service = world.get(AnotherService)

    @injector
    async def f(x: Provide[MyService]):
        return x

    res = await f()
    assert res is service

    class Dummy:
        @injector
        async def method(self, x: Provide[MyService]):
            return x

        @injector
        @classmethod
        async def klass(cls, x: Provide[MyService]):
            return x

        @injector
        @staticmethod
        async def static(x: Provide[MyService]):
            return x

    dummy = Dummy()
    print(dir(dummy))
    res = await dummy.method()
    assert res is service
    res = await dummy.klass()
    assert res is service
    res = await dummy.static()
    assert res is service
    res = await Dummy.klass()
    assert res is service
    res = await Dummy.static()
    assert res is service

    @injector(dependencies=(MyService, AnotherService))
    async def f(x, y):
        return x, y

    res = await f()
    assert res == (service, another_service)
    res = await f(y=1)
    assert res == (service, 1)
    res = await f(1)
    assert res == (1, another_service)
    res = await f(1, 2)
    assert res == (1, 2)

    @injector(dependencies=(MyService, "unknown"))
    async def f(x, y):
        return x, y

    with pytest.raises(DependencyNotFoundError, match=".*unknown.*"):
        await f()

    @injector(dependencies=(MyService,))
    async def f(x, y):
        return x, y

    with pytest.raises(TypeError):
        await f()

    @injector(dependencies=(MyService, "unknown"))
    async def f(x, y=None):
        return x, y

    res = await f()
    assert res == (service, None)


def test_dependencies_shortcut():
    @inject([MyService])
    def f(x):
        return x

    assert f() is world.get[MyService]()

    @inject(dict(x=MyService))
    def g(x):
        return x

    assert g() is world.get[MyService]()

    with pytest.raises(TypeError):

        @inject("test")
        def h(x):
            return x

    with pytest.raises(TypeError):

        @inject([MyService], dependencies=dict())
        def h2(x):
            return x

    with pytest.raises(TypeError):

        @inject(dict(x=MyService), dependencies=dict())
        def h3(x):
            return x


def test_ignore_type_hints():
    with pytest.raises(NameError, match=".*name 'A' is not defined.*"):

        @inject
        def f(a: "A", my_service: MyService = inject.get(MyService)):
            return my_service

    @inject(ignore_type_hints=True)
    def g(a: "A", my_service: MyService = inject.get(MyService)):
        return my_service

    class A:
        pass

    assert g(A()) is world.get[MyService]()


def test_no_injection_error_when_ignoring_type_hints():
    with pytest.raises(RuntimeError, match="(?i)No dependencies found.*"):

        @inject(ignore_type_hints=True)
        def f():
            pass

    with pytest.raises(NoInjectionsFoundError, match="(?i)No dependencies found.*"):

        @inject(ignore_type_hints=True)
        def g():
            pass


def test_wrapped_injection():
    @inject
    def f(my_service: MyService = inject.get(MyService)) -> MyService:
        return my_service

    assert f() is world.get[MyService]()

    @functools.wraps(f)
    def g() -> MyService:
        return f()

    with pytest.raises(DoubleInjectionError):
        inject(g)

    assert g() is world.get[MyService]()

import functools
import itertools
from typing import Iterator, Optional, Sequence, Union

import pytest
from typing_extensions import Annotated

from antidote import From, FromArg, Get, world
from antidote.core.annotations import Provide
from antidote.core.exceptions import NoInjectionsFoundError
from antidote.core.injection import inject, validate_injection
from antidote.exceptions import DependencyNotFoundError, DoubleInjectionError


class MyService:
    pass


class AnotherService:
    pass


@pytest.fixture(autouse=True)
def setup_world() -> Iterator[None]:
    with world.test.empty():
        world.test.singleton({MyService: MyService(), AnotherService: AnotherService()})
        yield


SENTINEL = "SENTINEL"


class DoesNotRaise:
    def __enter__(self):
        pass

    def __exit__(self, exc_type, exc_val, exc_tb):
        pass


@pytest.mark.parametrize(
    "kwargs, expectation",
    [
        pytest.param(kwargs, expectation, id=str(kwargs))
        for kwargs, expectation in [
            (dict(), DoesNotRaise()),
            (dict(dependencies=lambda arg: arg.name), DoesNotRaise()),
            (dict(dependencies=dict(x="x")), DoesNotRaise()),
            (dict(dependencies=["x"]), DoesNotRaise()),
            (dict(auto_provide=True), DoesNotRaise()),
            (
                dict(dependencies="{arg_name}"),
                pytest.raises(TypeError, match=".*dependencies.*str.*"),
            ),
            (dict(dependencies=1), pytest.raises(TypeError, match=".*dependencies.*int.*")),
            (dict(auto_provide=1), pytest.raises(TypeError, match=".*auto_provide.*int.*")),
            (dict(auto_provide=["x"]), pytest.raises(TypeError, match=".*auto_provide.*x.*")),
        ]
    ],
)
def test_validate_injection(kwargs, expectation):
    with expectation:

        @inject(**kwargs)
        def f(x: MyService):
            pass

    with expectation:
        validate_injection(**kwargs)


@pytest.fixture(params=[inject])
def injector(request):
    return request.param


def test_simple(injector):
    @injector
    def f(x: Provide[MyService]):
        return x

    with world.test.empty():
        s = MyService()
        world.test.singleton(MyService, s)
        assert s == f()


no_SENTINEL_injection = [
    pytest.param((None, None), dict(), id="nothing"),
    pytest.param(
        (MyService, None), dict(dependencies=dict(first=MyService)), id="dependencies:dict-first"
    ),
    pytest.param((MyService, None), dict(dependencies=(MyService,)), id="dependencies:tuple-first"),
    pytest.param(
        (None, MyService), dict(dependencies=dict(second=MyService)), id="dependencies:dict-second"
    ),
    pytest.param(
        (None, MyService), dict(dependencies=(None, MyService)), id="dependencies:tuple-second"
    ),
    pytest.param(
        ("first", "second"), dict(dependencies=lambda arg: arg.name), id="dependencies:callable"
    ),
    pytest.param(
        (MyService, MyService),
        dict(dependencies=lambda arg: MyService),
        id="dependencies:callable2",
    ),
    pytest.param((None, None), dict(dependencies=lambda s: None), id="dependencies:callable3"),
]


@pytest.mark.parametrize("expected,kwargs", no_SENTINEL_injection)
def test_without_type_hints(injector, expected, kwargs):
    @injector(**kwargs)
    def f(first=SENTINEL, second=SENTINEL):
        return first, second

    class A:
        @injector(**kwargs)
        def method(self, first=SENTINEL, second=SENTINEL):
            return first, second

        @injector(**kwargs)
        @classmethod
        def class_method(cls, first=SENTINEL, second=SENTINEL):
            return first, second

        @injector(**kwargs)
        @staticmethod
        def static_method(first=SENTINEL, second=SENTINEL):
            return first, second

        @classmethod
        @injector(**kwargs)
        def klass(cls, first=SENTINEL, second=SENTINEL):
            return first, second

    world.test.singleton(
        {"first": object(), "second": object(), "prefix:first": object(), "prefix:second": object()}
    )

    expected = tuple((world.get(d) if d is not None else SENTINEL for d in expected))
    (first, second) = expected
    a, b = object(), object()

    assert (a, second) == f(first=a)
    assert (a, second) == A().method(first=a)
    assert (a, second) == A.class_method(first=a)
    assert (a, second) == A.static_method(first=a)
    assert (a, second) == A.klass(first=a)

    assert (first, b) == f(second=b)
    assert (first, b) == A().method(second=b)
    assert (first, b) == A.class_method(second=b)
    assert (first, b) == A.static_method(second=b)
    assert (first, b) == A.klass(second=b)

    assert (a, second) == f(a)
    assert (a, second) == A().method(a)
    assert (a, second) == A.class_method(a)
    assert (a, second) == A.static_method(a)
    assert (a, second) == A.klass(a)

    assert (a, b) == f(a, b)
    assert (a, b) == A().method(a, b)
    assert (a, b) == A.class_method(a, b)
    assert (a, b) == A.static_method(a, b)
    assert (a, b) == A.klass(a, b)


@pytest.mark.parametrize(
    "expected,kwargs",
    no_SENTINEL_injection
    + [
        pytest.param((MyService, AnotherService), dict(auto_provide=True), id="auto_provide:True"),
        pytest.param((None, None), dict(auto_provide=False), id="auto_provide:False"),
        pytest.param(
            (MyService, None), dict(auto_provide=[MyService]), id="auto_provide:list-first"
        ),
        pytest.param(
            (None, AnotherService),
            dict(auto_provide=[AnotherService]),
            id="auto_provide:list-second",
        ),
        pytest.param((None, None), dict(auto_provide=[]), id="auto_provide:list-None"),
        pytest.param(
            (MyService, AnotherService),
            dict(auto_provide=lambda cls: True),
            id="auto_provide:func-True",
        ),
        pytest.param(
            (None, None), dict(auto_provide=lambda cls: False), id="auto_provide:func-False"
        ),
        pytest.param(
            (MyService, None),
            dict(auto_provide=lambda cls: issubclass(cls, MyService)),
            id="auto_provide:func-first",
        ),
        pytest.param(
            (None, AnotherService),
            dict(auto_provide=lambda cls: issubclass(cls, AnotherService)),
            id="auto_provide:func-second",
        ),
        pytest.param(
            (MyService, MyService),
            dict(dependencies=dict(second=MyService), auto_provide=True),
            id="auto_provide&dependencies:second",
        ),
        pytest.param(
            (None, MyService),
            dict(dependencies=dict(second=MyService), auto_provide=[AnotherService]),
            id="auto_provide&dependencies:second",
        ),
    ],
)
def test_with_auto_provide(injector, expected, kwargs):
    @injector(**kwargs)
    def f(first: MyService = SENTINEL, second: AnotherService = SENTINEL):
        return first, second

    class A:
        @injector(**kwargs)
        def method(self, first: MyService = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

        @injector(**kwargs)
        @classmethod
        def class_method(cls, first: MyService = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

        @injector(**kwargs)
        @staticmethod
        def static_method(first: MyService = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

        @classmethod
        @injector(**kwargs)
        def klass(cls, first: MyService = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

    world.test.singleton(
        {
            "first": "first",
            "second": "second",
            "prefix:first": "prefix:first",
            "prefix:second": "prefix:second",
        }
    )

    expected = tuple((world.get(d) if d is not None else SENTINEL for d in expected))
    (first, second) = expected
    a, b = "a", "b"

    assert (a, second) == f(first=a)
    assert (a, second) == A().method(first=a)
    assert (a, second) == A.class_method(first=a)
    assert (a, second) == A.static_method(first=a)
    assert (a, second) == A.klass(first=a)

    assert (first, b) == f(second=b)
    assert (first, b) == A().method(second=b)
    assert (first, b) == A.class_method(second=b)
    assert (first, b) == A.static_method(second=b)
    assert (first, b) == A.klass(second=b)

    assert (a, second) == f(a)
    assert (a, second) == A().method(a)
    assert (a, second) == A.class_method(a)
    assert (a, second) == A.static_method(a)
    assert (a, second) == A.klass(a)

    assert (a, b) == f(a, b)
    assert (a, b) == A().method(a, b)
    assert (a, b) == A.class_method(a, b)
    assert (a, b) == A.static_method(a, b)
    assert (a, b) == A.klass(a, b)


@pytest.mark.parametrize(
    "expected, kwargs",
    [
        pytest.param((MyService, None), dict(), id="nothing"),
        pytest.param(
            (MyService, None),
            dict(dependencies=dict(first=AnotherService)),
            id="dependencies:dict-first",
        ),
        pytest.param(
            (MyService, None), dict(dependencies=(AnotherService,)), id="dependencies:tuple-first"
        ),
        pytest.param(
            (MyService, MyService),
            dict(dependencies=dict(second=MyService)),
            id="dependencies:dict-second",
        ),
        pytest.param(
            (MyService, MyService),
            dict(dependencies=(None, MyService)),
            id="dependencies:tuple-second",
        ),
        pytest.param(
            (MyService, "second"),
            dict(dependencies=lambda arg: arg.name),
            id="dependencies:callable",
        ),
        pytest.param(
            (MyService, AnotherService),
            dict(dependencies=lambda arg: AnotherService),
            id="dependencies:callable2",
        ),
        pytest.param(
            (MyService, None), dict(dependencies=lambda arg: None), id="dependencies:callable3"
        ),
        pytest.param((MyService, AnotherService), dict(auto_provide=True), id="auto_provide:True"),
        pytest.param((MyService, None), dict(auto_provide=False), id="auto_provide:False"),
        pytest.param(
            (MyService, None), dict(auto_provide=[MyService]), id="auto_provide:list-first"
        ),
        pytest.param(
            (MyService, AnotherService),
            dict(auto_provide=[AnotherService]),
            id="auto_provide:list-second",
        ),
        pytest.param(
            (MyService, None),
            dict(auto_provide=lambda cls: issubclass(cls, MyService)),
            id="auto_provide:func-first",
        ),
        pytest.param(
            (MyService, AnotherService),
            dict(auto_provide=lambda cls: issubclass(cls, AnotherService)),
            id="auto_provide:func-second",
        ),
    ],
)
def test_with_provide(injector, expected, kwargs):
    @injector(**kwargs)
    def f(first: Provide[MyService] = SENTINEL, second: AnotherService = SENTINEL):
        return first, second

    class A:
        @injector(**kwargs)
        def method(self, first: Provide[MyService] = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

        @injector(**kwargs)
        @classmethod
        def class_method(
            cls, first: Provide[MyService] = SENTINEL, second: AnotherService = SENTINEL
        ):
            return first, second

        @injector(**kwargs)
        @staticmethod
        def static_method(first: Provide[MyService] = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

        @classmethod
        @injector(**kwargs)
        def klass(cls, first: Provide[MyService] = SENTINEL, second: AnotherService = SENTINEL):
            return first, second

    world.test.singleton(
        {"first": object(), "second": object(), "prefix:first": object(), "prefix:second": object()}
    )

    expected = tuple((world.get(d) if d is not None else SENTINEL for d in expected))
    (first, second) = expected
    a, b = object(), object()

    assert (a, second) == f(first=a)
    assert (a, second) == A().method(first=a)
    assert (a, second) == A.class_method(first=a)
    assert (a, second) == A.static_method(first=a)
    assert (a, second) == A.klass(first=a)

    assert (first, b) == f(second=b)
    assert (first, b) == A().method(second=b)
    assert (first, b) == A.class_method(second=b)
    assert (first, b) == A.static_method(second=b)
    assert (first, b) == A.klass(second=b)

    assert (a, second) == f(a)
    assert (a, second) == A().method(a)
    assert (a, second) == A.class_method(a)
    assert (a, second) == A.static_method(a)
    assert (a, second) == A.klass(a)

    assert (a, b) == f(a, b)
    assert (a, b) == A().method(a, b)
    assert (a, b) == A.class_method(a, b)
    assert (a, b) == A.static_method(a, b)
    assert (a, b) == A.klass(a, b)


@pytest.mark.parametrize(
    "type_hint",
    # builtins
    [str, int, float, set, list, dict, complex, type, tuple, bytes, bytearray]
    # typing
    + [Optional, Sequence]
    # not a class / weird stuff
    + [1, lambda x: x, object()],
)
def test_ignored_type_hints(injector, type_hint):
    @injector(auto_provide=True)
    def f(x: type_hint):
        pass

    world.test.singleton(type_hint, object())
    with pytest.raises(TypeError):
        f()


def test_none_optional_support(injector):
    class Dummy:
        pass

    @injector
    def f(x: Provide[MyService] = None):
        return x

    @injector
    def g(x: Optional[Provide[MyService]] = None):
        return x

    with world.test.empty():
        s = MyService()
        world.test.singleton(Dummy, Dummy())
        world.test.singleton(MyService, s)
        assert f() is s
        assert g() is s

    with world.test.empty():
        world.test.singleton(Dummy, Dummy())
        assert f() is None
        assert g() is None


@pytest.mark.parametrize("auto_provide", [True, False])
def test_none_optional_support_auto_provide(injector, auto_provide):
    class Dummy:
        pass

    @injector(auto_provide=auto_provide)
    def f(x: MyService = None):
        return x

    @injector(auto_provide=auto_provide)
    def g(x: Optional[MyService] = None):
        return x

    @injector(auto_provide=auto_provide)
    def h(x: Union[MyService, Dummy]):
        pass

    with world.test.empty():
        s = MyService()
        world.test.singleton(Dummy, Dummy())
        world.test.singleton(MyService, s)
        expected = s if auto_provide else None
        assert f() is expected
        assert g() is expected
        with pytest.raises(TypeError):
            h()

    with world.test.empty():
        world.test.singleton(Dummy, Dummy())
        assert f() is None
        assert g() is None


def test_annotations(injector):
    class Dummy:
        pass

    with world.test.empty():

        @injector
        def custom_annotated(x: Annotated[Dummy, object()]):
            return x

        world.test.singleton(Dummy, Dummy())
        with pytest.raises(TypeError):
            custom_annotated()

    with world.test.empty():

        @injector
        def optional_annotated(x: Annotated[Dummy, object()] = None):
            return x

        world.test.singleton(Dummy, Dummy())
        assert optional_annotated() is None

    with world.test.empty():

        @injector
        def get(x: Annotated[Dummy, Get("dummy")]):
            return x

        world.test.singleton("dummy", Dummy())
        assert get() is world.get("dummy")

    with world.test.empty():

        @injector
        def optional_get(x: Optional[Annotated[Dummy, Get("dummy")]] = None):
            return x

        world.test.singleton("dummy", Dummy())
        assert optional_get() is world.get("dummy")

    with world.test.empty():

        @injector
        def from_arg(x: Annotated[Dummy, FromArg(lambda arg: arg.name)]):
            return x

        world.test.singleton("x", Dummy())
        assert from_arg() is world.get("x")

    with world.test.empty():

        class Maker:
            def __rmatmul__(self, other):
                return "dummy"

        @injector
        def get(x: Annotated[Dummy, From(Maker())]):
            return x

        world.test.singleton("dummy", Dummy())
        assert get() is world.get("dummy")


def test_multiple_antidote_annotations(injector):
    class Dummy:
        pass

    class Maker:
        def __rmatmul__(self, other):
            return "dummy"

    annotations = [
        Get("dummy"),
        From(Maker()),
        FromArg(lambda arg: arg.name),
    ]
    for (a, b) in itertools.combinations(annotations, 2):
        with pytest.raises(TypeError):

            @injector
            def custom_annotated(x: Annotated[Dummy, a, b]):
                return x


@pytest.fixture(params=["function", "method", "classmethod", "classmethod-after", "staticmethod"])
def injected_method_with(request, injector):
    kind = request.param

    def builder(**kwargs):
        if kind == "function":

            @injector(**kwargs)
            def f(x: MyService):
                return x

            return f
        else:

            class Dummy:
                @injector(**kwargs)
                def method(self, x: MyService):
                    return x

                @classmethod
                @injector(**kwargs)
                def class_method(cls, x: MyService):
                    return x

                @injector(**kwargs)
                @classmethod
                def class_method_after(cls, x: MyService):
                    return x

                @injector(**kwargs)
                @staticmethod
                def static_method(x: MyService):
                    return x

            if kind == "method":
                return Dummy().method
            elif kind == "classmethod":
                return Dummy.class_method
            elif kind == "classmethod-after":
                return Dummy.class_method_after
            elif kind == "staticmethod":
                return Dummy.static_method
            else:
                raise RuntimeError()

    return builder


@pytest.mark.parametrize(
    "expectation,kwargs",
    [
        pytest.param(
            pytest.raises(TypeError, match=".*dependencies.*"),
            dict(dependencies=object()),
            id="dependencies:unsupported-type",
        ),
        pytest.param(
            pytest.raises(TypeError),
            dict(dependencies={1: "x"}),
            id="dependencies:invalid-key-type",
        ),
        pytest.param(
            pytest.raises(TypeError, match=".*ignore_type_hints.*"),
            dict(ignore_type_hints=object()),
            id="ignore_type_hints:unsupported-type",
        ),
        pytest.param(
            pytest.raises(TypeError, match=".*auto_provide.*"),
            dict(auto_provide=object()),
            id="auto_provide:unsupported-type",
        ),
        pytest.param(
            pytest.raises(TypeError, match=".*strict_validation.*"),
            dict(strict_validation=object()),
            id="auto_provide:unsupported-type",
        ),
    ],
)
def test_invalid_args(injected_method_with, expectation, kwargs):
    with expectation:
        injected_method_with(**kwargs)()

    with expectation:
        validate_injection(**kwargs)


@pytest.mark.parametrize(
    "expectation,kwargs",
    [
        pytest.param(pytest.raises(TypeError), dict(), id="nothing"),
        pytest.param(
            pytest.raises(DependencyNotFoundError, match=".*Service.*"),
            dict(dependencies=(MyService,)),
            id="dependencies:tuple",
        ),
        pytest.param(
            pytest.raises(DependencyNotFoundError, match=".*Service.*"),
            dict(dependencies=dict(x=MyService)),
            id="dependencies:dict",
        ),
        pytest.param(
            pytest.raises(DependencyNotFoundError, match=".*Service.*"),
            dict(dependencies=lambda arg: MyService),
            id="dependencies:callable",
        ),
        pytest.param(pytest.raises(TypeError), dict(auto_provide=False), id="auto_provide:False"),
        pytest.param(
            pytest.raises(DependencyNotFoundError, match=".*Service.*"),
            dict(auto_provide=True),
            id="auto_provide:True",
        ),
        pytest.param(
            pytest.raises(DependencyNotFoundError, match=".*Service.*"),
            dict(auto_provide=[MyService]),
            id="auto_provide:list",
        ),
    ],
)
def test_unknown_dependency(injected_method_with, expectation, kwargs):
    with world.test.empty():
        with expectation:
            injected_method_with(**kwargs)()


def test_unknown_provide(injector):
    with world.test.empty():

        @injector
        def f(x: Provide[MyService]):
            return x

        with pytest.raises(DependencyNotFoundError):
            f()


@pytest.mark.parametrize(
    "expectation,kwargs",
    [
        pytest.param(
            pytest.raises(ValueError, match=".*y.*"),
            dict(auto_provide=[AnotherService]),
            id="auto_provide",
        ),
        pytest.param(
            pytest.raises(ValueError, match=".*y.*"),
            dict(auto_provide=[MyService, AnotherService]),
            id="auto_provide2",
        ),
        pytest.param(
            pytest.raises(ValueError),
            dict(dependencies=(None, None)),
            id="dependencies:too-much-arguments",
        ),
        pytest.param(
            pytest.raises(ValueError, match=".*unknown.*"),
            dict(dependencies=dict(unknown=DependencyNotFoundError)),
            id="dependencies:unknown-dict-key",
        ),
    ],
)
@pytest.mark.parametrize("strict", [True, False])
def test_strict_validation(injected_method_with, injector, expectation, kwargs, strict):
    kwargs["strict_validation"] = strict
    if strict:
        with expectation:
            injected_method_with(**kwargs)
    else:
        injected_method_with(**kwargs)(object())


@pytest.mark.parametrize(
    "expectation,kwargs",
    [
        pytest.param(
            pytest.raises(ValueError, match=".*self.*"),
            dict(dependencies=dict(self="x")),
            id="dependencies",
        ),
    ],
)
def test_cannot_inject_self(injector, expectation, kwargs):
    with expectation:

        class A:
            @injector(**kwargs)
            def method(self, x=None):
                return x

        A()

    with expectation:

        class A:
            @injector(**kwargs)
            @classmethod
            def classmethod(self, x=None):
                return x

        A()

    with expectation:

        class A:
            @classmethod
            @injector(**kwargs)
            def classmethod(self, x=None):
                return x

        A()


def test_no_injections():
    def f(x):
        return x

    # When nothing can be injected, the same function should be returned
    assert inject(f) is f


def test_double_injection(injector):
    # When the function has already its arguments injected, the same function should
    # be returned
    with pytest.raises(DoubleInjectionError, match=".*<locals>.f.*"):

        @injector
        @injector(auto_provide=True)
        def f(x: MyService):
            return x

    with pytest.raises(DoubleInjectionError, match=".*StaticmethodA.*"):

        class StaticmethodA:
            @injector
            @staticmethod
            @injector(auto_provide=True)
            def f(x, y: MyService):
                pass

    with pytest.raises(DoubleInjectionError, match=".*StaticmethodB.*"):

        class StaticmethodB:
            @injector
            @injector(auto_provide=True)
            @staticmethod
            def f(x, y: MyService):
                pass

    with pytest.raises(DoubleInjectionError, match=".*ClassmethodA.*"):

        class ClassmethodA:
            @injector
            @classmethod
            @injector(auto_provide=True)
            def f(cls, x: MyService):
                pass

    with pytest.raises(DoubleInjectionError, match=".*ClassmethodB.*"):

        class ClassmethodB:
            @injector
            @injector(auto_provide=True)
            @classmethod
            def f(cls, x: MyService):
                pass

    with pytest.raises(DoubleInjectionError, match=".*Method.*"):

        class Method:
            @injector
            @injector(auto_provide=True)
            def f(self, x: MyService):
                pass


def test_invalid_inject(injector):
    with pytest.raises(TypeError):

        @injector
        class Dummy:
            pass

    with pytest.raises(TypeError):
        injector(1)


def test_static_class_method(injector):
    class Dummy:
        @injector(auto_provide=True)
        @staticmethod
        def static(x: MyService):
            pass

        @injector(auto_provide=True)
        @classmethod
        def klass(cls, x: MyService):
            pass

    assert isinstance(Dummy.__dict__["static"], staticmethod)
    assert isinstance(Dummy.__dict__["klass"], classmethod)


@pytest.mark.asyncio
async def test_async(injector):
    service = world.get(MyService)
    another_service = world.get(AnotherService)

    @injector
    async def f(x: Provide[MyService]):
        return x

    res = await f()
    assert res is service

    class Dummy:
        @injector
        async def method(self, x: Provide[MyService]):
            return x

        @injector
        @classmethod
        async def klass(cls, x: Provide[MyService]):
            return x

        @injector
        @staticmethod
        async def static(x: Provide[MyService]):
            return x

    dummy = Dummy()
    print(dir(dummy))
    res = await dummy.method()
    assert res is service
    res = await dummy.klass()
    assert res is service
    res = await dummy.static()
    assert res is service
    res = await Dummy.klass()
    assert res is service
    res = await Dummy.static()
    assert res is service

    @injector(dependencies=(MyService, AnotherService))
    async def f(x, y):
        return x, y

    res = await f()
    assert res == (service, another_service)
    res = await f(y=1)
    assert res == (service, 1)
    res = await f(1)
    assert res == (1, another_service)
    res = await f(1, 2)
    assert res == (1, 2)

    @injector(dependencies=(MyService, "unknown"))
    async def f(x, y):
        return x, y

    with pytest.raises(DependencyNotFoundError, match=".*unknown.*"):
        await f()

    @injector(dependencies=(MyService,))
    async def f(x, y):
        return x, y

    with pytest.raises(TypeError):
        await f()

    @injector(dependencies=(MyService, "unknown"))
    async def f(x, y=None):
        return x, y

    res = await f()
    assert res == (service, None)


def test_dependencies_shortcut():
    @inject([MyService])
    def f(x):
        return x

    assert f() is world.get[MyService]()

    @inject(dict(x=MyService))
    def g(x):
        return x

    assert g() is world.get[MyService]()

    with pytest.raises(TypeError):

        @inject("test")
        def h(x):
            return x

    with pytest.raises(TypeError):

        @inject([MyService], dependencies=dict())
        def h2(x):
            return x

    with pytest.raises(TypeError):

        @inject(dict(x=MyService), dependencies=dict())
        def h3(x):
            return x


def test_ignore_type_hints():
    with pytest.raises(NameError, match=".*name 'A' is not defined.*"):

        @inject
        def f(a: "A", my_service: MyService = inject.get(MyService)):
            return my_service

    @inject(ignore_type_hints=True)
    def g(a: "A", my_service: MyService = inject.get(MyService)):
        return my_service

    class A:
        pass

    assert g(A()) is world.get[MyService]()


def test_no_injection_error_when_ignoring_type_hints():
    with pytest.raises(RuntimeError, match="(?i)No dependencies found.*"):

        @inject(ignore_type_hints=True)
        def f():
            pass

    with pytest.raises(NoInjectionsFoundError, match="(?i)No dependencies found.*"):

        @inject(ignore_type_hints=True)
        def g():
            pass


def test_wrapped_injection():
    @inject
    def f(my_service: MyService = inject.get(MyService)) -> MyService:
        return my_service

    assert f() is world.get[MyService]()

    @functools.wraps(f)
    def g() -> MyService:
        return f()

    with pytest.raises(DoubleInjectionError):
        inject(g)

    assert g() is world.get[MyService]()
# builtins # typing # not a class / weird stuff # When nothing can be injected, the same function should be returned # When the function has already its arguments injected, the same function should # be returned