from typing import Annotated, Any

import pytest

from dijay import (
    REQUEST,
    TRANSIENT,
    Inject,
    injectable,
    instance,
    resolve,
)


class Base:
    pass


@injectable(Base)
class Implementation(Base):
    pass


@injectable()
class Service:
    pass


@injectable(scope=TRANSIENT)
class TransientService:
    pass


@injectable(scope=REQUEST)
class RequestService:
    pass


@injectable()
async def async_factory() -> str:

    return "async_ready"


@pytest.mark.asyncio
async def test_singleton():

    s1 = await resolve(Service)

    s2 = await resolve(Service)

    assert s1 is s2


@pytest.mark.asyncio
async def test_transient():

    t1 = await resolve(TransientService)

    t2 = await resolve(TransientService)

    assert t1 is not t2


@pytest.mark.asyncio
async def test_request_scope():

    r1 = await resolve(RequestService, id="req_a")

    r2 = await resolve(RequestService, id="req_a")

    r3 = await resolve(RequestService, id="req_b")

    assert r1 is r2

    assert r1 is not r3


@pytest.mark.asyncio
async def test_abstract_injection():

    obj = await resolve(Base)

    assert isinstance(obj, Implementation)


@pytest.mark.asyncio
async def test_async_factory_resolution():

    val = await resolve(str)
    assert val == "async_ready"


@pytest.mark.asyncio
async def test_constructor_injection():

    @injectable()
    class App:
        def __init__(self, service: Service, data: str):

            self.service = service

            self.data = data

    app = await resolve(App)

    assert isinstance(app.service, Service)
    assert app.data == "async_ready"


@pytest.mark.asyncio
async def test_annotated_token_injection():
    c = instance()

    @c.injectable("API_URL")
    def url_factory() -> str:

        return "http://localhost"

    @c.injectable()
    class Client:
        def __init__(self, url: Annotated[str, Inject("API_URL")]):

            self.url = url

    client = await c.resolve(Client)

    assert client.url == "http://localhost"


@pytest.mark.asyncio
async def test_lifecycle_context_manager():
    c = instance()

    events = []

    @c.on_bootstrap
    async def boot():

        events.append("boot")

    @c.on_shutdown
    def shut():

        events.append("shut")

    async with c:
        assert "boot" in events

        assert "shut" not in events

    assert "shut" in events


@pytest.mark.asyncio
async def test_register_binds_implementation():
    c = instance()

    class Repo:
        pass

    class FakeRepo(Repo):
        pass

    c.register(Repo, FakeRepo)
    repo = await c.resolve(Repo)

    assert isinstance(repo, FakeRepo)


@pytest.mark.asyncio
async def test_register_conditional():
    c = instance()

    class Storage:
        pass

    class MemoryStorage(Storage):
        pass

    class DiskStorage(Storage):
        pass

    provider_map = {"memory": MemoryStorage, "disk": DiskStorage}

    selected = provider_map.get("memory")
    c.register(Storage, selected)

    storage = await c.resolve(Storage)

    assert isinstance(storage, MemoryStorage)


@pytest.mark.asyncio
async def test_register_transient_scope():
    c = instance()

    class Svc:
        pass

    c.register(Svc, Svc, scope=TRANSIENT)

    s1 = await c.resolve(Svc)
    s2 = await c.resolve(Svc)

    assert s1 is not s2


@pytest.mark.asyncio
async def test_circular_dependency_raises():
    c = instance()

    @c.injectable()
    class A:
        def __init__(self, b: B):
            pass

    @c.injectable()
    class B:
        def __init__(self, a: A):
            pass

    with pytest.raises(RuntimeError, match="Circular dependency"):
        await c.resolve(A)


@pytest.mark.asyncio
async def test_unregistered_class_auto_resolved():
    c = instance()

    class Standalone:
        pass

    obj = await c.resolve(Standalone)
    assert isinstance(obj, Standalone)


@pytest.mark.asyncio
async def test_unregistered_non_class_raises():
    c = instance()

    with pytest.raises(RuntimeError, match="não registrado"):
        await c.resolve("unknown_token")


@pytest.mark.asyncio
async def test_any_hint_skipped():
    c = instance()

    @c.injectable()
    class WithAny:
        def __init__(self, x: Any = "default"):
            self.x = x

    obj = await c.resolve(WithAny)
    assert obj.x == "default"


@pytest.mark.asyncio
async def test_optional_dependency_fallback():
    c = instance()

    class Missing:
        pass

    @c.injectable()
    class WithOptional:
        def __init__(self, dep: Missing | None = None):
            self.dep = dep

    obj = await c.resolve(WithOptional)
    assert obj.dep is None


@pytest.mark.asyncio
async def test_annotated_without_inject():
    c = instance()

    c.register("my_value", lambda: 42)

    @c.injectable()
    class WithAnnotated:
        def __init__(self, val: Annotated[int, "just_metadata"]):
            self.val = val

    obj = await c.resolve(WithAnnotated)
    assert isinstance(obj.val, int)
