from typing import Dict, Hashable, Optional

import pytest

from antidote.core.container import Container, DependencyValue, RawContainer, RawProvider, Scope
from antidote.core.exceptions import DuplicateDependencyError
from antidote.core.utils import DependencyDebug
from antidote.exceptions import (
    DependencyCycleError,
    DependencyInstantiationError,
    DependencyNotFoundError,
    FrozenWorldError,
)
from .utils import DummyFactoryProvider, DummyProvider


class A:
    def __init__(self, *args):
        pass


class B:
    def __init__(self, *args):
        pass


class C:
    def __init__(self, *args):
        pass


class ServiceWithNonMetDependency:
    def __init__(self, dependency):
        pass


@pytest.fixture()
def container():
    return RawContainer()


def test_dependency_repr():
    o = object()
    d = DependencyValue(o, scope=Scope.singleton())

    assert "singleton" in repr(d)
    assert repr(o) in repr(d)


def test_scope_repr():
    s = Scope("test")
    assert "test" in repr(s)
    assert "test" in str(s)


def test_get(container: RawContainer):
    container.add_provider(DummyFactoryProvider)
    container.get(DummyFactoryProvider).data = {
        A: lambda _: A(),
        ServiceWithNonMetDependency: lambda _: ServiceWithNonMetDependency(),
    }
    container.add_provider(DummyProvider)
    container.get(DummyProvider).data = {"name": "Antidote"}

    assert isinstance(container.get(A), A)
    assert isinstance(container.provide(A), DependencyValue)
    assert "Antidote" == container.get("name")
    assert "Antidote" == container.provide("name").unwrapped

    with pytest.raises(DependencyNotFoundError):
        container.get(object)

    with pytest.raises(DependencyInstantiationError):
        container.get(ServiceWithNonMetDependency)


def test_singleton(container: RawContainer):
    container.add_provider(DummyFactoryProvider)
    container.get(DummyFactoryProvider).data = {
        A: lambda _: A(),
        B: lambda _: B(),
    }

    service = container.get(A)
    assert container.get(A) is service
    assert container.provide(A).unwrapped is service
    assert container.provide(A).scope is Scope.singleton()

    container.get(DummyFactoryProvider).singleton = False
    another_service = container.get(B)
    assert container.get(B) is not another_service
    assert container.provide(B).unwrapped is not another_service
    assert container.provide(B).scope is None

    assert container.get(A) == service


def test_dependency_cycle_error(container: RawContainer):
    container.add_provider(DummyFactoryProvider)
    container.get(DummyFactoryProvider).data = {
        A: lambda _: container.get(B),
        B: lambda _: container.get(C),
        C: lambda _: container.get(A),
    }

    with pytest.raises(DependencyCycleError):
        container.get(A)

    with pytest.raises(DependencyCycleError):
        container.get(B)

    with pytest.raises(DependencyCycleError):
        container.get(C)


def test_dependency_instantiation_error(container: RawContainer):
    container.add_provider(DummyFactoryProvider)

    def raise_error():
        raise RuntimeError()

    container.get(DummyFactoryProvider).data = {
        A: lambda _: container.get(B),
        B: lambda _: container.get(C),
        C: lambda _: raise_error(),
    }

    with pytest.raises(DependencyInstantiationError, match=".*C.*"):
        container.get(C)

    with pytest.raises(DependencyInstantiationError, match=".*A.*"):
        container.get(A)


def test_providers_property(container: RawContainer):
    x = object()
    container.add_provider(DummyProvider)
    container.get(DummyProvider).data = dict(x=x)
    container.add_provider(DummyFactoryProvider)
    container.get(DummyFactoryProvider).data = dict(y=lambda c: c.get("y"))

    assert len(container.providers) == 2
    for provider in container.providers:
        if isinstance(provider, DummyProvider):
            assert provider.data == dict(x=x)
        else:
            assert "y" in provider.data


def test_scope_property(container: RawContainer):
    assert container.scopes == []

    s1 = container.create_scope("1")
    assert container.scopes == [s1]

    s2 = container.create_scope("2")
    assert container.scopes == [s1, s2]


def test_repr_str(container: RawContainer):
    container.add_provider(DummyProvider)
    container.get(DummyProvider).data = {"name": "Antidote"}

    assert repr(container.get(DummyProvider)) in repr(container)
    assert str(container.get(DummyProvider)) in str(container)


def test_freeze(container: RawContainer):
    container.add_provider(DummyProvider)
    container.get(DummyProvider).data = {"name": "Antidote"}
    container.freeze()

    with pytest.raises(FrozenWorldError):
        container.add_provider(DummyFactoryProvider)


def test_freezing_locked(container: RawContainer):
    with container.locked(freezing=True):
        pass

    container.freeze()

    with pytest.raises(FrozenWorldError):
        with container.locked(freezing=True):
            pass


def test_provider_property(container: RawContainer):
    container.add_provider(DummyProvider)
    assert container.providers == [container.get(DummyProvider)]


@pytest.mark.parametrize("singleton", [True, False])
def test_clone(container: RawContainer, singleton: bool):
    class A:
        pass

    container.add_provider(DummyFactoryProvider)
    provider = container.get(DummyFactoryProvider)
    provider.data = {"a": lambda c: A()}
    provider.singleton = singleton
    original = container.get("a")

    cloned = container.clone(keep_singletons=True)
    assert cloned.get(DummyFactoryProvider) is not provider
    assert cloned.get(DummyFactoryProvider).data == provider.data
    assert isinstance(cloned.get("a"), A)
    assert (cloned.get("a") is original) == singleton

    cloned = container.clone(keep_singletons=False)
    assert cloned.get(DummyFactoryProvider) is not provider
    assert cloned.get(DummyFactoryProvider).data == provider.data
    assert isinstance(cloned.get("a"), A)
    assert cloned.get("a") is not original


def test_providers_must_properly_clone(container: RawContainer):
    class DummySelf(RawProvider):
        def clone(self, keep_singletons_cache: bool) -> "RawProvider":
            return self

    container.add_provider(DummySelf)

    with pytest.raises(RuntimeError, match="(?i).*provider.*instance.*"):
        container.clone(keep_singletons=False, keep_scopes=False)


def test_providers_must_properly_clone2(container: RawContainer):
    container.add_provider(DummyProvider)
    p = container.get(DummyProvider)

    class DummyRegistered(RawProvider):
        def clone(self, keep_singletons_cache: bool) -> "RawProvider":
            return p

    container.add_provider(DummyRegistered)
    with pytest.raises(RuntimeError, match="(?i).*provider.*new instance.*"):
        container.clone(keep_singletons=False, keep_scopes=False)


@pytest.mark.filterwarnings("ignore:Debug information")
def test_raise_if_exists(container: RawContainer):
    container.raise_if_exists(object())  # Nothing should happen

    container.add_provider(DummyProvider)
    container.get(DummyProvider).data = {"name": "Antidote"}
    with pytest.raises(DuplicateDependencyError, match=".*DummyProvider.*"):
        container.raise_if_exists("name")

    class DummyProviderWithDebug(DummyProvider):
        def debug(self, dependency) -> DependencyDebug:
            return DependencyDebug("debug_info", scope=Scope.singleton())

    container.add_provider(DummyProviderWithDebug)
    container.get(DummyProviderWithDebug).data = {"hello": "world"}
    with pytest.raises(DuplicateDependencyError, match=".*DummyProviderWithDebug.*\\ndebug_info"):
        container.raise_if_exists("hello")


def test_scope(container: RawContainer):
    class ScopeProvider(RawProvider):
        dependencies: Dict[object, DependencyValue] = {}

        def exists(self, dependency):
            return dependency in self.dependencies

        def maybe_provide(
            self, dependency: Hashable, container: Container
        ) -> Optional[DependencyValue]:
            try:
                return self.dependencies[dependency]
            except KeyError:
                return None

    container.add_provider(ScopeProvider)

    scope = container.create_scope("dummy")
    x = object()
    y = object()
    ScopeProvider.dependencies[1] = DependencyValue(x, scope=scope)
    assert container.get(1) is x

    ScopeProvider.dependencies[1] = DependencyValue(y, scope=scope)
    # Using cache
    assert container.get(1) is x
    assert container.provide(1) == DependencyValue(x, scope=scope)

    container.reset_scope(scope)
    assert container.get(1) is y
    assert container.provide(1) == DependencyValue(y, scope=scope)


def test_sanity_checks(container: RawContainer):
    # Cannot register twice the same kind of provider
    container.add_provider(DummyProvider)
    with pytest.raises(AssertionError):
        container.add_provider(DummyProvider)

    container.create_scope("test")
    with pytest.raises(AssertionError):
        container.create_scope("test")


def test_already_frozen(container: RawContainer):
    container.freeze()

    with pytest.raises(FrozenWorldError):
        container.freeze()


def test_debug(container: RawContainer):
    with pytest.raises(DependencyNotFoundError):
        container.debug(object())
