from dataclasses import dataclass

import pytest

from autowired import (
    Context,
    autowired,
    cached_property,
    UnresolvableDependencyException,
    AmbiguousDependencyException,
    Container,
    InitializationError,
)

# noinspection PyProtectedMember
from autowired import Dependency, NotInstantiableException


class Service0:
    pass


@dataclass
class Service1:
    service0: Service0


class Service2:
    def __init__(self, service1: Service1, blabla: str = "blabla"):
        self.service1 = service1
        self.blabla = blabla


@dataclass
class Service3:
    service0: Service0
    service1: Service1
    foo: str = "bar"


##


class DemoContext(Context):
    service2: Service2 = autowired()

    @cached_property
    def service1(self) -> Service1:
        return self.autowire(Service1)

    @cached_property
    def service3(self) -> Service3:
        return self.autowire(Service3, foo="baz")


def test_service_instances():
    ctx = DemoContext()

    service2 = ctx.service2
    service3 = ctx.service3
    service2b = ctx.service2

    # Assert that the same instance is returned by the context
    assert id(service2) == id(service2b), "Different instances of service2"

    # Assert that property `foo` is overridden
    assert service3.foo == "baz", "Incorrect foo value"

    # Assert that service2 in service3 and service2 are same
    assert id(service3.service1) == id(ctx.service1)

    assert isinstance(ctx.service1, Service1)
    assert isinstance(ctx.service2, Service2)
    assert isinstance(ctx.service3, Service3)


@dataclass
class ServiceX:
    s: str


@dataclass
class Service4:
    x: ServiceX
    service1: Service1


def test_derived_context():
    ctx = DemoContext()

    class DerivedContext(Context):
        service4: Service4 = autowired()

        def __init__(self, parent_context: Context):
            self.parent_context = parent_context

        @cached_property
        def service_x(self) -> ServiceX:
            return ServiceX("x")

    derived_context = DerivedContext(ctx)

    assert isinstance(derived_context.service4, Service4)
    assert id(derived_context.service4.service1) == id(ctx.service1)
    assert isinstance(derived_context.service_x, ServiceX)


def test_unresolvable_dependency():
    class TestContext(Context):
        service1: Service4 = autowired()

    ctx = TestContext()

    with pytest.raises(UnresolvableDependencyException):
        print(ctx.service1)


def test_ambiguous_dependency():
    class TestContext(Context):
        service1_a: Service1 = autowired()
        service1_b: Service1 = autowired()

    ctx = TestContext()

    with pytest.raises(AmbiguousDependencyException):
        ctx.autowire(Service2)

    with pytest.raises(AmbiguousDependencyException):
        ctx.container.resolve(Dependency("service1", Service1, True))

    s1a = ctx.container.resolve(Dependency("service1_a", Service1, True))
    s1b = ctx.container.resolve(Dependency("service1_b", Service1, True))

    assert isinstance(s1a, Service1)
    assert isinstance(s1b, Service1)
    assert id(s1a) != id(s1b)


def test_not_instantiable():
    class TestContext(Context):
        service_x: ServiceX = autowired()

    ctx = TestContext()
    try:
        ctx.service_x
    except Exception as e:
        assert isinstance(e, UnresolvableDependencyException)
        cause = e.__cause__
        assert isinstance(cause, NotInstantiableException)


def test_resolve_by_type():
    ctx = DemoContext()
    service1 = ctx.container.resolve(Service1)

    assert id(ctx.service1) == id(service1)


def test_eager_init():
    names_initiated = []

    class TestService:
        def __init__(self, name: str):
            nonlocal names_initiated
            names_initiated.append(name)
            self.name = name

    class TestContext(Context):
        service1: TestService = autowired(eager=True, name="service1")
        service2: TestService = autowired(eager=False, name="service2")

    ctx = TestContext()

    assert len(names_initiated) == 1
    assert names_initiated[0] == "service1"

    service1 = ctx.service1

    assert len(names_initiated) == 1

    service2 = ctx.service2

    assert len(names_initiated) == 2

    assert service1.name == "service1"
    assert service2.name == "service2"


def test_autowire_initialization_error():
    container = Container()

    class TestService:
        def __init__(self):
            raise Exception("Test exception")

    with pytest.raises(InitializationError):
        container.autowire(TestService)


def test_unregister_dependency():
    container = Container()
    s0 = Service0()
    container.register(s0)

    s0_resolved = container.resolve(Service0)

    assert id(s0) == id(s0_resolved)

    container.unregister_by_name("service0")

    s0_resolved2 = container.resolve(Service0)

    assert id(s0) != id(s0_resolved2)
