from __future__ import annotations

from typing import Annotated, Any, cast

import pytest

import diwire
from diwire import Component, Container, FromContext, Injected, ResolverContext, Scope
from diwire.exceptions import (
    DIWireDependencyNotRegisteredError,
    DIWireInvalidRegistrationError,
    DIWireResolverNotSetError,
)


class _Service:
    def __init__(self, value: str) -> None:
        self.value = value


def test_top_level_resolver_context_export_is_available() -> None:
    assert isinstance(diwire.resolver_context, ResolverContext)


def test_resolver_context_does_not_expose_resolver_stack_mutators() -> None:
    context = ResolverContext()

    assert not hasattr(context, "push_resolver")
    assert not hasattr(context, "pop_resolver")
    assert not hasattr(context, "wrap_resolver")


def test_resolve_raises_when_no_bound_resolver_and_no_fallback_container() -> None:
    context = ResolverContext()

    with pytest.raises(DIWireResolverNotSetError, match="resolver_context"):
        context.resolve(_Service)


@pytest.mark.asyncio
async def test_aresolve_raises_when_no_bound_resolver_and_no_fallback_container() -> None:
    context = ResolverContext()

    with pytest.raises(DIWireResolverNotSetError, match="resolver_context"):
        await context.aresolve(_Service)


def test_enter_scope_raises_when_no_bound_resolver_and_no_fallback_container() -> None:
    context = ResolverContext()

    with pytest.raises(DIWireResolverNotSetError, match="resolver_context"):
        context.enter_scope(Scope.REQUEST)


def test_resolve_uses_fallback_container_when_unbound() -> None:
    context = ResolverContext()
    container = Container(resolver_context=context)
    container.add_instance(_Service("fallback"), provides=_Service)

    assert context.resolve(_Service).value == "fallback"


@pytest.mark.asyncio
async def test_aresolve_uses_fallback_container_when_unbound() -> None:
    context = ResolverContext()
    container = Container(resolver_context=context)
    container.add_instance(_Service("fallback"), provides=_Service)

    assert (await context.aresolve(_Service)).value == "fallback"


def test_enter_scope_uses_fallback_container_when_unbound() -> None:
    context = ResolverContext()
    container = Container(resolver_context=context)
    container.add_instance(_Service("fallback"), provides=_Service)

    with context.enter_scope(Scope.REQUEST) as request_scope:
        assert request_scope.resolve(_Service).value == "fallback"
        with context.enter_scope(Scope.ACTION) as action_scope:
            assert action_scope.resolve(_Service).value == "fallback"
            with context.enter_scope(Scope.STEP) as step_scope:
                assert step_scope.resolve(_Service).value == "fallback"


def test_last_container_wins_for_fallback_resolve_and_enter_scope() -> None:
    context = ResolverContext()
    first = Container(resolver_context=context)
    first.add_instance(_Service("first"), provides=_Service)
    second = Container(resolver_context=context)
    second.add_instance(_Service("second"), provides=_Service)

    assert context.resolve(_Service).value == "second"
    with context.enter_scope(Scope.REQUEST) as request_scope:
        assert request_scope.resolve(_Service).value == "second"


@pytest.mark.asyncio
async def test_last_container_wins_for_fallback_aresolve() -> None:
    context = ResolverContext()
    first = Container(resolver_context=context)
    first.add_instance(_Service("first"), provides=_Service)
    second = Container(resolver_context=context)
    second.add_instance(_Service("second"), provides=_Service)

    assert (await context.aresolve(_Service)).value == "second"


def test_inject_raises_when_no_resolver_and_no_fallback_container() -> None:
    context = ResolverContext()

    @context.inject
    def handler(service: Injected[_Service]) -> str:
        return service.value

    with pytest.raises(DIWireResolverNotSetError, match=r"resolver_context\.inject"):
        cast("Any", handler)()


def test_inject_explicit_resolver_still_requires_fallback_container() -> None:
    context = ResolverContext()
    resolver = Container().compile()

    @context.inject
    def handler(service: Injected[_Service]) -> str:
        return service.value

    with pytest.raises(DIWireResolverNotSetError, match="fallback container"):
        cast("Any", handler)(diwire_resolver=resolver)


def test_inject_uses_fallback_container_when_unbound() -> None:
    context = ResolverContext()
    container = Container(resolver_context=context)
    container.add_instance(_Service("fallback"), provides=_Service)

    @context.inject
    def handler(service: Injected[_Service]) -> str:
        return service.value

    assert cast("Any", handler)() == "fallback"


def test_inject_requires_explicit_resolver_when_fallback_disables_resolver_context() -> None:
    context = ResolverContext()
    container = Container(resolver_context=context, use_resolver_context=False)
    container.add_instance(_Service("fallback"), provides=_Service)

    @context.inject
    def handler(service: Injected[_Service]) -> str:
        return service.value

    injected_handler = cast("Any", handler)
    with pytest.raises(DIWireResolverNotSetError, match="use_resolver_context=True"):
        injected_handler()

    with container.enter_scope(Scope.REQUEST) as request_scope:
        assert injected_handler(diwire_resolver=request_scope) == "fallback"


def test_last_container_wins_for_fallback_inject() -> None:
    context = ResolverContext()
    first = Container(resolver_context=context)
    first.add_instance(_Service("first"), provides=_Service)
    second = Container(resolver_context=context)
    second.add_instance(_Service("second"), provides=_Service)

    @context.inject
    def handler(service: Injected[_Service]) -> str:
        return service.value

    assert cast("Any", handler)() == "second"


def test_context_bound_resolver_takes_precedence_over_fallback_container() -> None:
    context = ResolverContext()
    first = Container(resolver_context=context)
    first.add_instance(_Service("first"), provides=_Service)
    second = Container(resolver_context=context)
    second.add_instance(_Service("second"), provides=_Service)

    @context.inject
    def handler(service: Injected[_Service]) -> str:
        return service.value

    assert cast("Any", handler)() == "second"

    with first.compile():
        assert cast("Any", handler)() == "first"


def test_context_bound_resolver_takes_precedence_over_fallback_for_resolve_and_enter_scope() -> (
    None
):
    context = ResolverContext()
    first = Container(resolver_context=context)
    first.add_instance(_Service("first"), provides=_Service)
    second = Container(resolver_context=context)
    second.add_instance(_Service("second"), provides=_Service)

    assert context.resolve(_Service).value == "second"
    with context.enter_scope(Scope.REQUEST) as request_scope:
        assert request_scope.resolve(_Service).value == "second"

    with first.compile():
        assert context.resolve(_Service).value == "first"
        with context.enter_scope(Scope.REQUEST) as request_scope:
            assert request_scope.resolve(_Service).value == "first"

    assert context.resolve(_Service).value == "second"


@pytest.mark.asyncio
async def test_context_bound_resolver_takes_precedence_over_fallback_for_aresolve() -> None:
    context = ResolverContext()
    first = Container(resolver_context=context)
    first.add_instance(_Service("first"), provides=_Service)
    second = Container(resolver_context=context)
    second.add_instance(_Service("second"), provides=_Service)

    assert (await context.aresolve(_Service)).value == "second"

    async with first.compile():
        assert (await context.aresolve(_Service)).value == "first"

    assert (await context.aresolve(_Service)).value == "second"


def test_nested_sync_context_manager_restores_previous_resolver() -> None:
    context = ResolverContext()
    first = Container(resolver_context=context)
    first.add_instance("first", provides=str)
    second = Container(resolver_context=context)
    second.add_instance("second", provides=str)

    with first.compile():
        assert context.resolve(str) == "first"
        with second.compile():
            assert context.resolve(str) == "second"
        assert context.resolve(str) == "first"

    assert context.resolve(str) == "second"


@pytest.mark.asyncio
async def test_nested_async_context_manager_restores_previous_resolver() -> None:
    context = ResolverContext()
    first = Container(resolver_context=context)
    first.add_instance("first", provides=str)
    second = Container(resolver_context=context)
    second.add_instance("second", provides=str)

    async with first.compile():
        assert await context.aresolve(str) == "first"
        async with second.compile():
            assert await context.aresolve(str) == "second"
        assert await context.aresolve(str) == "first"

    assert context.resolve(str) == "second"


def test_fallback_scope_binding_works_when_container_disables_resolver_context() -> None:
    context = ResolverContext()
    container = Container(resolver_context=context, use_resolver_context=False)
    container.add_instance(_Service("fallback"), provides=_Service)

    assert context.resolve(_Service).value == "fallback"

    with context.enter_scope(Scope.REQUEST) as request_scope:
        assert request_scope.resolve(_Service).value == "fallback"
        with context.enter_scope(Scope.ACTION) as action_scope:
            assert action_scope.resolve(_Service).value == "fallback"


def test_inject_rejects_reserved_resolver_parameter_name() -> None:
    context = ResolverContext()

    with pytest.raises(DIWireInvalidRegistrationError, match="cannot declare reserved parameter"):

        @context.inject
        def _handler(diwire_resolver: object, /, service: Injected[_Service]) -> None:
            _ = service


def test_inject_rejects_reserved_context_parameter_name() -> None:
    context = ResolverContext()

    with pytest.raises(DIWireInvalidRegistrationError, match="cannot declare reserved parameter"):

        @context.inject
        def _handler(diwire_context: object, /, service: Injected[_Service]) -> None:
            _ = service


def test_inject_forwards_context_kwarg_and_validates_scope_usage() -> None:
    context = ResolverContext()
    container = Container(resolver_context=context)

    @context.inject(scope=Scope.REQUEST)
    def handler(value: FromContext[int]) -> int:
        return value

    injected_handler = cast("Any", handler)
    assert injected_handler(diwire_context={int: 7}) == 7

    @context.inject(auto_open_scope=False)
    def no_scope(value: FromContext[int]) -> int:
        return value

    no_scope_handler = cast("Any", no_scope)
    with pytest.raises(
        DIWireInvalidRegistrationError,
        match="was provided but no new scope was opened",
    ):
        no_scope_handler(diwire_context={int: 9})


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

    with container.enter_scope(Scope.REQUEST, context={int: 9}) as request_scope:
        assert request_scope.resolve(FromContext[Annotated[int, "meta"]]) == 9


def test_scope_resolver_from_context_component_still_requires_component_key() -> None:
    component_key = Annotated[int, Component("priority")]
    container = Container()

    with container.enter_scope(Scope.REQUEST, context={component_key: 7, int: 1}) as request_scope:
        assert (
            request_scope.resolve(FromContext[Annotated[int, Component("priority"), "meta"]]) == 7
        )
        with pytest.raises(DIWireDependencyNotRegisteredError, match="Context value"):
            request_scope.resolve(FromContext[Annotated[int, Component("missing"), "meta"]])
