from __future__ import annotations

import inspect
from collections.abc import AsyncGenerator, Generator
from contextlib import asynccontextmanager, contextmanager
from types import TracebackType
from typing import Any, cast

import pytest

from diwire import Container
from diwire._internal.providers import ContextManagerProvider, FactoryProvider, GeneratorProvider
from diwire.exceptions import DIWireInvalidRegistrationError


class Service:
    pass


class ExplicitService:
    pass


class DecoratorService:
    pass


class DecoratorConcreteService:
    pass


class ClassContextManagerService:
    def __enter__(self) -> Any:
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_value: BaseException | None,
        traceback: TracebackType | None,
    ) -> bool | None:
        return None


TYPING_EXTENSIONS_SELF: Any = type("Self", (), {"__module__": "typing_extensions"})


class TypingExtensionsSelfCM:
    def __enter__(self) -> TYPING_EXTENSIONS_SELF:
        return self

    def __exit__(
        self,
        exc_type: type[BaseException] | None,
        exc_value: BaseException | None,
        traceback: TracebackType | None,
    ) -> bool | None:
        return None


class AsyncClassContextManagerService:
    async def __aenter__(self) -> Any:
        return self

    async def __aexit__(
        self,
        exc_type: type[BaseException] | None,
        exc_value: BaseException | None,
        traceback: TracebackType | None,
    ) -> bool | None:
        return None


ClassContextManagerService.__enter__.__annotations__["return"] = ClassContextManagerService
AsyncClassContextManagerService.__aenter__.__annotations__["return"] = (
    AsyncClassContextManagerService
)


def build_class_context_manager() -> ClassContextManagerService:
    return ClassContextManagerService()


def test_register_factory_without_provides_infers_registration_key() -> None:
    def build_service() -> Service:
        return Service()

    container = Container()

    container.add_factory(build_service)

    provider_spec = container._providers_registrations.get_by_type(Service)
    assert provider_spec.factory is build_service


def test_register_generator_without_provides_infers_registration_key() -> None:
    def build_service() -> Generator[Service, None, None]:
        try:
            yield Service()
        finally:
            pass

    container = Container()

    container.add_generator(build_service)

    provider_spec = container._providers_registrations.get_by_type(Service)
    assert provider_spec.generator is build_service


def test_register_context_manager_without_provides_infers_registration_key() -> None:
    @contextmanager
    def build_service() -> Generator[Service, None, None]:
        yield Service()

    container = Container()

    container.add_context_manager(build_service)

    provider_spec = container._providers_registrations.get_by_type(Service)
    assert provider_spec.context_manager is build_service


def test_register_context_manager_class_without_provides_infers_registration_key() -> None:
    container = Container()

    container.add_context_manager(ClassContextManagerService)

    provider_spec = container._providers_registrations.get_by_type(ClassContextManagerService)
    assert provider_spec.context_manager is ClassContextManagerService


def test_register_context_manager_provider_annotated_to_return_cm_class() -> None:
    container = Container()

    container.add_context_manager(build_class_context_manager)

    provider_spec = container._providers_registrations.get_by_type(ClassContextManagerService)
    assert provider_spec.context_manager is build_class_context_manager


def test_register_context_manager_class_with_typing_extensions_self_infers_registration_key() -> (
    None
):
    container = Container()

    container.add_context_manager(TypingExtensionsSelfCM)

    provider_spec = container._providers_registrations.get_by_type(TypingExtensionsSelfCM)
    assert provider_spec.context_manager is TypingExtensionsSelfCM


def test_register_async_context_manager_class_without_provides_infers_registration_key() -> None:
    container = Container()

    container.add_context_manager(AsyncClassContextManagerService)

    provider_spec = container._providers_registrations.get_by_type(AsyncClassContextManagerService)
    assert provider_spec.context_manager is AsyncClassContextManagerService
    assert provider_spec.is_async


def test_register_factory_without_provides_and_invalid_annotation_raises_error() -> None:
    def build_service() -> Service:
        return Service()

    build_service.__annotations__["return"] = "MissingService"

    container = Container()

    with pytest.raises(DIWireInvalidRegistrationError, match="factory provider"):
        container.add_factory(build_service)


def test_register_generator_without_provides_and_invalid_annotation_raises_error() -> None:
    def build_service() -> Service:
        return Service()

    container = Container()
    bad_generator = cast("GeneratorProvider[Any]", build_service)

    with pytest.raises(DIWireInvalidRegistrationError, match="generator provider"):
        container.add_generator(bad_generator)


def test_register_context_manager_without_provides_and_invalid_annotation_raises_error() -> None:
    def build_service() -> Service:
        return Service()

    container = Container()
    bad_context_manager = cast("ContextManagerProvider[Any]", build_service)

    with pytest.raises(DIWireInvalidRegistrationError, match="context manager provider"):
        container.add_context_manager(bad_context_manager)


def test_add_generator_rejects_sync_generator_without_try_finally_by_default() -> None:
    def provide_service() -> Generator[Service, None, None]:
        yield Service()

    container = Container()

    with pytest.raises(
        DIWireInvalidRegistrationError,
        match="must place every yield/yield from inside the body of a try block",
    ):
        container.add_generator(provide_service, provides=Service)


def test_add_generator_rejects_async_generator_without_try_finally_by_default() -> None:
    async def provide_service() -> AsyncGenerator[Service, None]:
        yield Service()

    container = Container()

    with pytest.raises(
        DIWireInvalidRegistrationError,
        match="must place every yield/yield from inside the body of a try block",
    ):
        container.add_generator(provide_service, provides=Service)


def test_add_generator_rejects_yield_inside_try_without_finally() -> None:
    def provide_service() -> Generator[Service, None, None]:
        try:
            yield Service()
        except Exception:
            return

    container = Container()

    with pytest.raises(
        DIWireInvalidRegistrationError,
        match="must place every yield/yield from inside the body of a try block",
    ):
        container.add_generator(provide_service, provides=Service)


def test_add_generator_rejects_yield_inside_except_even_with_finally() -> None:
    def provide_service() -> Generator[Service, None, None]:
        try:
            pass
        except Exception:
            yield Service()
        finally:
            pass

    container = Container()

    with pytest.raises(
        DIWireInvalidRegistrationError,
        match="must place every yield/yield from inside the body of a try block",
    ):
        container.add_generator(provide_service, provides=Service)


def test_add_generator_rejects_yield_inside_else_even_with_finally() -> None:
    def provide_service() -> Generator[Service, None, None]:
        try:
            pass
        except Exception:
            return
        else:
            yield Service()
        finally:
            pass

    container = Container()

    with pytest.raises(
        DIWireInvalidRegistrationError,
        match="must place every yield/yield from inside the body of a try block",
    ):
        container.add_generator(provide_service, provides=Service)


def test_add_generator_rejects_yield_inside_finally_block() -> None:
    def provide_service() -> Generator[Service, None, None]:
        try:
            pass
        finally:
            yield Service()

    container = Container()

    with pytest.raises(
        DIWireInvalidRegistrationError,
        match="must place every yield/yield from inside the body of a try block",
    ):
        container.add_generator(provide_service, provides=Service)


def test_add_generator_accepts_yield_inside_try_with_except_else_finally() -> None:
    def provide_service() -> Generator[Service, None, None]:
        try:
            yield Service()
        except Exception:
            return
        else:
            _ = Service
        finally:
            pass

    container = Container()
    container.add_generator(provide_service, provides=Service)

    provider_spec = container._providers_registrations.get_by_type(Service)
    assert provider_spec.generator is provide_service


def test_add_generator_ignores_nested_function_and_class_scopes_for_validation() -> None:
    def provide_service() -> Generator[Service, None, None]:
        def _nested_generator() -> Generator[Service, None, None]:
            yield Service()

        class _Nested:
            def create(self) -> Generator[Service, None, None]:
                yield Service()

        _ = _nested_generator
        _ = _Nested
        try:
            yield Service()
        finally:
            pass

    container = Container()
    container.add_generator(provide_service, provides=Service)

    provider_spec = container._providers_registrations.get_by_type(Service)
    assert provider_spec.generator is provide_service


def test_add_generator_accepts_sync_generator_with_try_finally() -> None:
    def provide_service() -> Generator[Service, None, None]:
        try:
            yield Service()
        finally:
            pass

    container = Container()
    container.add_generator(provide_service, provides=Service)

    provider_spec = container._providers_registrations.get_by_type(Service)
    assert provider_spec.generator is provide_service


def test_add_generator_accepts_async_generator_with_try_finally() -> None:
    async def provide_service() -> AsyncGenerator[Service, None]:
        try:
            yield Service()
        finally:
            pass

    container = Container()
    container.add_generator(provide_service, provides=Service)

    provider_spec = container._providers_registrations.get_by_type(Service)
    assert provider_spec.generator is provide_service


def test_add_generator_can_opt_out_of_try_finally_validation() -> None:
    def provide_service() -> Generator[Service, None, None]:
        yield Service()

    container = Container()
    container.add_generator(
        provide_service,
        provides=Service,
        require_generator_finally=False,
    )

    provider_spec = container._providers_registrations.get_by_type(Service)
    assert provider_spec.generator is provide_service


def test_add_generator_fails_closed_when_source_inspection_fails_and_opt_out_bypasses(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    def provide_service() -> Generator[Service, None, None]:
        try:
            yield Service()
        finally:
            pass

    def _raise_source_error(_target: object) -> str:
        msg = "source unavailable"
        raise OSError(msg)

    container = Container()
    monkeypatch.setattr(inspect, "getsource", _raise_source_error)

    with pytest.raises(
        DIWireInvalidRegistrationError,
        match="could not inspect generator provider",
    ):
        container.add_generator(provide_service, provides=Service)

    container.add_generator(
        provide_service,
        provides=ExplicitService,
        require_generator_finally=False,
    )

    provider_spec = container._providers_registrations.get_by_type(ExplicitService)
    assert provider_spec.generator is provide_service


def test_add_generator_fails_closed_when_source_has_multiple_non_matching_functions(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    def provide_service() -> Generator[Service, None, None]:
        try:
            yield Service()
        finally:
            pass

    def _source_with_multiple_functions(_target: object) -> str:
        return "def first() -> int:\n    return 1\n\ndef second() -> int:\n    return 2\n"

    container = Container()
    monkeypatch.setattr(inspect, "getsource", _source_with_multiple_functions)

    with pytest.raises(
        DIWireInvalidRegistrationError,
        match="could not inspect generator provider",
    ):
        container.add_generator(provide_service, provides=Service)


def test_add_generator_uses_single_function_source_fallback_when_name_does_not_match(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    def provide_service() -> Generator[Service, None, None]:
        try:
            yield Service()
        finally:
            pass

    def _single_non_matching_source(_target: object) -> str:
        return (
            "def different_name() -> Generator[Service, None, None]:\n"
            "    try:\n"
            "        yield Service()\n"
            "    finally:\n"
            "        pass\n"
        )

    container = Container()
    monkeypatch.setattr(inspect, "getsource", _single_non_matching_source)
    container.add_generator(provide_service, provides=Service)

    provider_spec = container._providers_registrations.get_by_type(Service)
    assert provider_spec.generator is provide_service


def test_explicit_provides_bypasses_return_type_inference_for_all_provider_kinds() -> None:
    def bad_factory() -> Service:
        return Service()

    def bad_generator() -> Service:
        return Service()

    def bad_context_manager() -> Service:
        return Service()

    @asynccontextmanager
    async def valid_context_manager() -> AsyncGenerator[Service, None]:
        yield Service()

    bad_factory.__annotations__["return"] = "MissingFactoryType"

    container = Container()
    typed_bad_factory = cast("FactoryProvider[ExplicitService]", bad_factory)
    typed_bad_generator = cast("GeneratorProvider[Any]", bad_generator)
    typed_bad_context_manager = cast("ContextManagerProvider[Any]", bad_context_manager)

    container.add_factory(typed_bad_factory, provides=ExplicitService)
    factory_spec = container._providers_registrations.get_by_type(ExplicitService)
    assert factory_spec.factory is typed_bad_factory

    container.add_generator(typed_bad_generator, provides=Service)
    generator_spec = container._providers_registrations.get_by_type(Service)
    assert generator_spec.generator is typed_bad_generator

    container.add_context_manager(
        typed_bad_context_manager,
        provides=Service,
    )
    context_spec = container._providers_registrations.get_by_type(Service)
    assert context_spec.context_manager is typed_bad_context_manager

    typed_valid_cm = cast("ContextManagerProvider[Any]", valid_context_manager)
    container.add_context_manager(typed_valid_cm, provides=ExplicitService)
    async_context_spec = container._providers_registrations.get_by_type(ExplicitService)
    assert async_context_spec.context_manager is typed_valid_cm


def test_add_without_required_provider_argument_raises_type_error() -> None:
    container = Container()

    with pytest.raises(TypeError):
        cast("Any", container).add()


def test_add_factory_without_required_provider_argument_raises_type_error() -> None:
    container = Container()

    with pytest.raises(TypeError):
        cast("Any", container).add_factory()


def test_add_generator_without_required_provider_argument_raises_type_error() -> None:
    container = Container()

    with pytest.raises(TypeError):
        cast("Any", container).add_generator()


def test_add_context_manager_without_required_provider_argument_raises_type_error() -> None:
    container = Container()

    with pytest.raises(TypeError):
        cast("Any", container).add_context_manager()


def test_register_concrete_with_provides_only_uses_the_same_concrete_type() -> None:
    container = Container()

    container.add(DecoratorConcreteService)

    provider_spec = container._providers_registrations.get_by_type(DecoratorConcreteService)
    assert provider_spec.concrete_type is DecoratorConcreteService


def test_add_returns_none_and_registers_provider() -> None:
    container = Container()

    result = cast("Any", container).add(
        DecoratorConcreteService,
        provides=DecoratorConcreteService,
    )
    assert result is None
    provider_spec = container._providers_registrations.get_by_type(DecoratorConcreteService)
    assert provider_spec.concrete_type is DecoratorConcreteService


def test_add_generator_returns_none_and_registers_provider() -> None:
    def build_service() -> Generator[DecoratorService, None, None]:
        try:
            yield DecoratorService()
        finally:
            pass

    container = Container()

    result = cast("Any", container).add_generator(build_service, provides=DecoratorService)
    assert result is None
    provider_spec = container._providers_registrations.get_by_type(DecoratorService)
    assert provider_spec.generator is build_service


def test_add_context_manager_returns_none_and_registers_provider() -> None:
    @contextmanager
    def build_service() -> Generator[DecoratorService, None, None]:
        yield DecoratorService()

    container = Container()

    result = cast("Any", container).add_context_manager(
        build_service,
        provides=DecoratorService,
    )
    assert result is None
    provider_spec = container._providers_registrations.get_by_type(DecoratorService)
    assert provider_spec.context_manager is build_service
