from __future__ import annotations

import inspect
import threading
import typing
from collections.abc import AsyncGenerator, Generator
from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager, contextmanager
from dataclasses import dataclass
from types import SimpleNamespace
from typing import Annotated, Any, Generic, TypeVar, cast

import pytest

from diwire import (
    Component,
    Container,
    Injected,
    Lifetime,
    LockMode,
    ResolverContext,
    Scope,
    resolver_context,
)
from diwire._internal.injection import INJECT_WRAPPER_MARKER
from diwire._internal.providers import ProviderDependency
from diwire.exceptions import (
    DIWireAsyncDependencyInSyncContextError,
    DIWireDependencyNotRegisteredError,
    DIWireInvalidGenericTypeArgumentError,
    DIWireScopeMismatchError,
)

T = TypeVar("T")
U = TypeVar("U")


class _IBox(Generic[T]):
    pass


@dataclass(slots=True, kw_only=True)
class _KeywordBox(Generic[T]):
    type: type[T]


@dataclass(slots=True)
class _KeywordBoxImpl(_KeywordBox[T]):
    pass


@dataclass(slots=True)
class _KeywordSpecialIntBox(_KeywordBox[int]):
    pass


@dataclass
class _Box(_IBox[T]):
    type: type[T]


@dataclass
class _BoxA(_IBox[T]):
    type: type[T]


@dataclass
class _BoxB(_IBox[T]):
    type: type[T]


@dataclass
class _NoArgBox(_IBox[T]):
    pass


class _SpecialIntBox(_IBox[int]):
    def __init__(self) -> None:
        self.type = int


def _create_box(type_arg: type[T]) -> _IBox[T]:
    return _Box(type=type_arg)


def _create_box_positional_only(type_arg: type[T], /) -> _IBox[T]:
    return _Box(type=type_arg)


async def _create_box_async(type_arg: type[T]) -> _IBox[T]:
    return _Box(type=type_arg)


def _generate_box(type_arg: type[T]) -> Generator[_IBox[T], None, None]:
    yield _Box(type=type_arg)


@contextmanager
def _context_box(type_arg: type[T]) -> Generator[_IBox[T], None, None]:
    yield _Box(type=type_arg)


@asynccontextmanager
async def _async_context_box(type_arg: type[T]) -> AsyncGenerator[_IBox[T], None]:
    yield _Box(type=type_arg)


def test_open_key_canonicalization_allows_latest_override_for_equivalent_keys() -> None:
    container = Container()
    container.add(_BoxA, provides=_IBox)
    container.add(_BoxB, provides=_IBox[T])

    resolved = container.resolve(_IBox[int])

    assert isinstance(resolved, _BoxB)


def test_open_concrete_registration_resolves_closed_generic_requests() -> None:
    container = Container()
    container.add(_Box, provides=_IBox)

    resolved = container.resolve(_IBox[str])

    assert isinstance(resolved, _Box)
    assert resolved.type is str
    materialized = container._providers_registrations.find_by_type(_IBox[str])
    assert materialized is not None
    assert materialized.factory is not None


def test_open_concrete_registration_resolves_non_component_annotated_closed_key() -> None:
    container = Container()
    container.add(_Box, provides=_IBox)

    resolved = container.resolve(Annotated[_IBox[int], "meta"])

    assert isinstance(resolved, _Box)
    assert resolved.type is int


def test_closed_registration_wins_over_open_template() -> None:
    container = Container()
    container.add(_Box, provides=_IBox)
    container.add(_SpecialIntBox, provides=_IBox[int])

    int_box = container.resolve(_IBox[int])
    str_box = container.resolve(_IBox[str])

    assert isinstance(int_box, _SpecialIntBox)
    assert isinstance(str_box, _Box)
    assert str_box.type is str


def test_closed_generic_override_with_kw_only_dataclass_typevar_field_is_resolvable() -> None:
    container = Container()
    container.add(_KeywordBoxImpl, provides=_KeywordBox)
    container.add(_KeywordSpecialIntBox, provides=_KeywordBox[int])

    str_box = container.resolve(_KeywordBox[str])
    int_box = container.resolve(_KeywordBox[int])

    assert isinstance(str_box, _KeywordBoxImpl)
    assert str_box.type is str
    assert isinstance(int_box, _KeywordSpecialIntBox)
    assert int_box.type is int


def test_open_factory_registration_supports_type_argument_injection() -> None:
    container = Container()
    container.add_factory(_create_box, provides=_IBox)

    assert cast("Any", container.resolve(_IBox[int])).type is int
    assert cast("Any", container.resolve(_IBox[str])).type is str
    materialized = container._providers_registrations.find_by_type(_IBox[int])
    assert materialized is not None
    assert materialized.factory is not None


def test_open_factory_materialized_wrapper_uses_bind_partial_fallback_for_positional_only() -> None:
    container = Container()
    container.add_factory(_create_box_positional_only, provides=_IBox)

    resolved = container.resolve(_IBox[int])

    assert isinstance(resolved, _Box)
    assert resolved.type is int
    materialized = container._providers_registrations.find_by_type(_IBox[int])
    assert materialized is not None
    assert materialized.factory is not None


def test_open_generator_registration_supports_type_argument_injection() -> None:
    container = Container()
    container.add_generator(_generate_box, provides=_IBox)

    resolved = container.resolve(_IBox[bytes])
    assert isinstance(resolved, _Box)
    assert resolved.type is bytes
    materialized = container._providers_registrations.find_by_type(_IBox[bytes])
    assert materialized is not None
    assert materialized.generator is not None


def test_open_context_manager_registration_works_inside_resolver_context() -> None:
    container = Container()
    container.add_context_manager(_context_box, provides=_IBox)

    with container as resolver:
        resolved = resolver.resolve(_IBox[float])
        assert isinstance(resolved, _Box)
        assert resolved.type is float
    materialized = container._providers_registrations.find_by_type(_IBox[float])
    assert materialized is not None
    assert materialized.context_manager is not None


def test_open_generic_scope_resolver_close_runs_cleanup_via_wrapper_delegate() -> None:
    events: list[str] = []

    def _tracked_open_generator(type_arg: type[T]) -> Generator[_IBox[T], None, None]:
        events.append(f"enter-{type_arg.__name__}")
        try:
            yield _Box(type=type_arg)
        finally:
            events.append(f"exit-{type_arg.__name__}")

    container = Container()
    container.add_generator(
        _tracked_open_generator,
        provides=_IBox,
        scope=Scope.REQUEST,
        lifetime=Lifetime.SCOPED,
    )

    request_scope = container.enter_scope()

    resolved = request_scope.resolve(_IBox[int])
    assert isinstance(resolved, _Box)
    assert resolved.type is int

    request_scope.close()
    assert events == ["enter-int", "exit-int"]


@pytest.mark.asyncio
async def test_open_async_context_manager_registration_works_in_async_path() -> None:
    container = Container()
    container.add_context_manager(_async_context_box, provides=_IBox)

    async with container as resolver:
        resolved = await resolver.aresolve(_IBox[float])
        assert isinstance(resolved, _Box)
        assert resolved.type is float
    materialized = container._providers_registrations.find_by_type(_IBox[float])
    assert materialized is not None
    assert materialized.context_manager is not None


@pytest.mark.asyncio
async def test_open_async_factory_materializes_closed_key() -> None:
    container = Container()
    container.add_factory(_create_box_async, provides=_IBox)

    resolved = await container.aresolve(_IBox[int])

    assert isinstance(resolved, _Box)
    assert resolved.type is int
    materialized = container._providers_registrations.find_by_type(_IBox[int])
    assert materialized is not None
    assert materialized.factory is not None


@pytest.mark.asyncio
async def test_open_generic_scope_resolver_aclose_runs_cleanup_via_wrapper_delegate() -> None:
    events: list[str] = []

    @asynccontextmanager
    async def _tracked_open_async_context(type_arg: type[T]) -> AsyncGenerator[_IBox[T], None]:
        events.append(f"enter-{type_arg.__name__}")
        try:
            yield _Box(type=type_arg)
        finally:
            events.append(f"exit-{type_arg.__name__}")

    container = Container()
    container.add_context_manager(
        _tracked_open_async_context,
        provides=_IBox,
        scope=Scope.REQUEST,
        lifetime=Lifetime.SCOPED,
    )

    request_scope = container.enter_scope()

    resolved = await request_scope.aresolve(_IBox[int])
    assert isinstance(resolved, _Box)
    assert resolved.type is int

    await request_scope.aclose()
    assert events == ["enter-int", "exit-int"]


def test_open_async_factory_raises_in_sync_resolution() -> None:
    container = Container()
    container.add_factory(_create_box_async, provides=_IBox)

    with pytest.raises(DIWireAsyncDependencyInSyncContextError, match="requires asynchronous"):
        container.resolve(_IBox[int])


class _Repo(Generic[T]):
    pass


@dataclass
class _GenericRepo(_Repo[T]):
    dependency_type: type[T]


@dataclass
class _ListRepo(_Repo[list[U]]):
    item_type: type[U]


def test_most_specific_open_template_wins_for_matching_request() -> None:
    container = Container()
    container.add(_GenericRepo, provides=_Repo)
    container.add(_ListRepo, provides=_Repo[list[U]])

    resolved_specific = container.resolve(_Repo[list[int]])
    resolved_fallback = container.resolve(_Repo[str])

    assert isinstance(resolved_specific, _ListRepo)
    assert resolved_specific.item_type is int
    assert isinstance(resolved_fallback, _GenericRepo)
    assert resolved_fallback.dependency_type is str


class _Model:
    pass


class _User(_Model):
    pass


M = TypeVar("M", bound=_Model)


class _ModelBox(Generic[M]):
    pass


@dataclass
class _DefaultModelBox(_ModelBox[M]):
    type: type[M]


def test_typevar_bound_is_validated_at_resolve_time() -> None:
    container = Container()
    container.add(_DefaultModelBox, provides=_ModelBox)

    valid = container.resolve(_ModelBox[_User])
    assert isinstance(valid, _DefaultModelBox)
    assert valid.type is _User

    invalid_key = cast("Any", _ModelBox)[str]
    with pytest.raises(DIWireInvalidGenericTypeArgumentError, match="bound"):
        container.resolve(invalid_key)
    assert container._providers_registrations.find_by_type(invalid_key) is None


def test_materialized_closed_key_is_purged_before_registration_mutation() -> None:
    container = Container()
    container.add_instance("keep", provides=str)
    container.add(_BoxA, provides=_IBox)

    first = container.resolve(_IBox[int])
    assert isinstance(first, _BoxA)
    assert container._providers_registrations.find_by_type(_IBox[int]) is not None

    container.add(_BoxB, provides=_IBox)
    assert container._providers_registrations.find_by_type(_IBox[int]) is None
    assert container.resolve(str) == "keep"

    second = container.resolve(_IBox[int])
    assert isinstance(second, _BoxB)


def test_materialized_open_concrete_without_type_argument_dependencies_stays_concrete() -> None:
    container = Container()
    container.add(_NoArgBox, provides=_IBox)

    resolved = container.resolve(_IBox[int])

    assert isinstance(resolved, _NoArgBox)
    materialized = container._providers_registrations.find_by_type(_IBox[int])
    assert materialized is not None
    assert materialized.concrete_type is _NoArgBox


def test_open_generic_dispatch_cache_is_used_after_first_materialized_resolve(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    container = Container()
    container.add_factory(_create_box, provides=_IBox)

    registry = container._open_generic_registry
    find_best_match_call_count = 0
    original_find_best_match = registry.find_best_match

    def _counting_find_best_match(dependency: Any) -> Any:
        nonlocal find_best_match_call_count
        find_best_match_call_count += 1
        return original_find_best_match(dependency)

    monkeypatch.setattr(registry, "find_best_match", _counting_find_best_match)

    first = container.resolve(_IBox[int])
    second = container.resolve(_IBox[int])

    assert isinstance(first, _Box)
    assert isinstance(second, _Box)
    assert find_best_match_call_count == 1


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

    def _inject_wrapped_factory(type_arg: type[T]) -> _IBox[T]:
        return _Box(type=type_arg)

    wrapped_factory = resolver_context.inject(_inject_wrapped_factory)
    container.add_factory(wrapped_factory, provides=_IBox)

    resolved = container.resolve(_IBox[int])

    assert isinstance(resolved, _Box)
    materialized = container._providers_registrations.find_by_type(_IBox[int])
    assert materialized is not None
    assert materialized.factory is not None
    assert bool(getattr(materialized.factory, INJECT_WRAPPER_MARKER, False))


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

    def _invalid_factory(type_arg: type[T], value: list[U]) -> _IBox[T]:
        _ = value
        return _Box(type=type_arg)

    container.add_factory(_invalid_factory, provides=_IBox)

    with pytest.raises(DIWireInvalidGenericTypeArgumentError, match="unresolved TypeVars"):
        container.resolve(_IBox[int])
    assert container._providers_registrations.find_by_type(_IBox[int]) is None


def test_materialization_callback_noops_when_closed_dependency_is_already_registered() -> None:
    container = Container()
    container.add_instance(1, provides=int)

    dummy_match = SimpleNamespace(spec=object(), typevar_map={})
    container._materialize_closed_open_generic_spec(int, dummy_match)

    assert container.resolve(int) == 1


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

    def _factory(dummy: int) -> _IBox[int]:
        _ = dummy
        return _Box(type=int)

    parameter = inspect.signature(_factory).parameters["dummy"]
    binding = SimpleNamespace(
        kind="generic_argument",
        typevar=None,
        dependency=ProviderDependency(provides=int, parameter=parameter),
    )
    spec = SimpleNamespace(
        bindings=(binding,),
        provider_kind="factory",
        provider=_factory,
        provider_is_inject_wrapper=False,
        lifetime=Lifetime.SCOPED,
        scope=Scope.APP,
        is_async=False,
        is_any_dependency_async=False,
        needs_cleanup=False,
        lock_mode=LockMode.NONE,
    )
    match = SimpleNamespace(spec=spec, typevar_map={})

    container._materialize_closed_open_generic_spec(_IBox[int], match)
    assert container._providers_registrations.find_by_type(_IBox[int]) is None


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

    def _factory(type_arg: type[T]) -> _IBox[T]:
        return _Box(type=type_arg)

    parameter = inspect.signature(_factory).parameters["type_arg"]
    typevar = TypeVar("typevar")
    binding = SimpleNamespace(
        kind="generic_argument",
        typevar=typevar,
        dependency=ProviderDependency(provides=type[T], parameter=parameter),
    )
    spec = SimpleNamespace(
        bindings=(binding,),
        provider_kind="factory",
        provider=_factory,
        provider_is_inject_wrapper=False,
        lifetime=Lifetime.SCOPED,
        scope=Scope.APP,
        is_async=False,
        is_any_dependency_async=False,
        needs_cleanup=False,
        lock_mode=LockMode.NONE,
    )
    match = SimpleNamespace(spec=spec, typevar_map={})

    container._materialize_closed_open_generic_spec(_IBox[int], match)
    assert container._providers_registrations.find_by_type(_IBox[int]) is None


def test_injected_open_generic_uses_open_resolver_fallback() -> None:
    container = Container()
    container.add(_Box, provides=_IBox)

    @resolver_context.inject
    def handler(box: Injected[_IBox[str]]) -> str:
        resolved_box = cast("Any", box)
        return resolved_box.type.__name__

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


def test_resolver_context_fallback_uses_latest_canonical_open_key() -> None:
    context = ResolverContext()
    runtime = Container(resolver_context=context)
    runtime.add(_BoxA, provides=_IBox)
    runtime.add(_BoxB, provides=_IBox[T])

    @context.inject
    def handler(box: Injected[_IBox[int]]) -> _IBox[int]:
        return box

    resolved = cast("Any", handler)()
    assert isinstance(resolved, _BoxB)


def test_open_singleton_cache_isolated_per_closed_dependency_key() -> None:
    container = Container()
    container.add_factory(_create_box, provides=_IBox, lifetime=Lifetime.SCOPED)

    int_first = container.resolve(_IBox[int])
    int_second = container.resolve(_IBox[int])
    str_box = container.resolve(_IBox[str])

    assert int_first is int_second
    assert cast("object", int_first) is not cast("object", str_box)


def test_open_scoped_cache_isolated_per_scope_and_closed_dependency_key() -> None:
    container = Container()
    container.add_factory(
        _create_box,
        provides=_IBox,
        scope=Scope.REQUEST,
        lifetime=Lifetime.SCOPED,
    )

    with pytest.raises(DIWireScopeMismatchError, match="requires opened scope level"):
        container.resolve(_IBox[int])

    with container.enter_scope() as request_one:
        one_first = request_one.resolve(_IBox[int])
        one_second = request_one.resolve(_IBox[int])
        one_str = request_one.resolve(_IBox[str])

    with container.enter_scope() as request_two:
        two_first = request_two.resolve(_IBox[int])

    assert one_first is one_second
    assert cast("object", one_first) is not cast("object", one_str)
    assert cast("object", one_first) is not cast("object", two_first)


def test_open_scoped_cache_works_when_entering_action_scope_directly() -> None:
    container = Container()
    container.add_factory(
        _create_box,
        provides=_IBox,
        scope=Scope.REQUEST,
        lifetime=Lifetime.SCOPED,
    )

    with container.enter_scope(Scope.ACTION) as action_scope:
        first = action_scope.resolve(_IBox[int])
        second = action_scope.resolve(_IBox[int])
        other = action_scope.resolve(_IBox[str])

    with container.enter_scope(Scope.ACTION) as next_action_scope:
        next_scope_first = next_action_scope.resolve(_IBox[int])

    assert first is second
    assert cast("object", first) is not cast("object", other)
    assert cast("object", first) is not cast("object", next_scope_first)


def test_resolving_open_generic_without_type_arguments_remains_unregistered() -> None:
    container = Container()
    container.add(_Box, provides=_IBox)

    with pytest.raises(DIWireDependencyNotRegisteredError):
        container.resolve(_IBox)


def test_autoregister_skips_open_generic_dependencies_when_match_exists() -> None:
    container = Container()
    container.add(_Box, provides=_IBox)

    resolved = container.resolve(_IBox[int])

    assert isinstance(resolved, _Box)
    assert resolved.type is int


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

    def _consumer(dep: str) -> None:
        _ = dep

    dependency = ProviderDependency(
        provides=str,
        parameter=inspect.signature(_consumer).parameters["dep"],
    )

    injected, remaining = container._resolve_closed_concrete_generic_injections(
        provides=_IBox[int],
        dependencies=[dependency],
    )

    assert injected == {}
    assert remaining == [dependency]
    assert container._closed_generic_typevar_map(provides=typing.Sequence) == {}
    assert container._closed_generic_typevar_map(
        provides=Annotated[_IBox[int], Component("primary")],
    ) == {T: int}
    assert container._closed_generic_typevar_map(provides=tuple[int]) == {}
    assert container._closed_generic_typevar_map(provides=inspect.Signature) == {}
    assert (
        container._resolve_closed_generic_injection_value(
            dependency_annotation=T,
            typevar_map={T: int},
        )
        is int
    )
    assert (
        container._resolve_closed_generic_injection_value(
            dependency_annotation=str,
            typevar_map={T: int},
        )
        is not int
    )


def test_runtime_materialization_serializes_provider_registration_add_calls(
    monkeypatch: pytest.MonkeyPatch,
) -> None:
    container = Container()
    container.add_factory(
        _create_box,
        provides=_IBox,
        lifetime=Lifetime.TRANSIENT,
        scope=Scope.APP,
    )
    container.compile()

    active_add_calls = 0
    observed_overlap = False
    active_add_calls_lock = threading.Lock()
    start_event = threading.Event()
    original_add = container._providers_registrations.add

    def _tracked_add(spec: Any) -> None:
        nonlocal active_add_calls, observed_overlap
        start_event.wait(timeout=5)
        with active_add_calls_lock:
            active_add_calls += 1
            if active_add_calls > 1:
                observed_overlap = True
        try:
            original_add(spec)
        finally:
            with active_add_calls_lock:
                active_add_calls -= 1

    monkeypatch.setattr(container._providers_registrations, "add", _tracked_add)

    dependencies = [type(f"_ConcurrentType{index}", (), {}) for index in range(32)]

    def _resolve_dependency(dependency: type[Any]) -> Any:
        return container.resolve(_IBox[dependency])

    with ThreadPoolExecutor(max_workers=16) as executor:
        futures = [executor.submit(_resolve_dependency, dependency) for dependency in dependencies]
        start_event.set()
        for future in futures:
            future.result()

    assert observed_overlap is False
    for dependency in dependencies:
        assert container._providers_registrations.find_by_type(_IBox[dependency]) is not None


def test_compile_is_safe_during_concurrent_runtime_materialization() -> None:
    container = Container()
    container.add_factory(
        _create_box,
        provides=_IBox,
        lifetime=Lifetime.TRANSIENT,
        scope=Scope.APP,
    )
    container.compile()

    failures: list[BaseException] = []
    failures_lock = threading.Lock()
    dependencies = [type(f"_CompileRaceType{index}", (), {}) for index in range(48)]

    def _record_failure(error: BaseException) -> None:
        with failures_lock:
            failures.append(error)

    def _resolve_dependency(dependency: type[Any]) -> None:
        try:
            container.resolve(_IBox[dependency])
        except BaseException as error:
            _record_failure(error)

    def _compile_many_times() -> None:
        try:
            for _ in range(200):
                container.compile()
        except BaseException as error:
            _record_failure(error)

    with ThreadPoolExecutor(max_workers=20) as executor:
        compile_future = executor.submit(_compile_many_times)
        resolve_futures = [
            executor.submit(_resolve_dependency, dependency) for dependency in dependencies
        ]
        compile_future.result()
        for resolve_future in resolve_futures:
            resolve_future.result()

    assert failures == []
