from copy import copy
try:
    # Python 2 only:
    from StringIO import StringIO

    # create a variant that can serve as a context manager
    class StringIO(StringIO):
        def __enter__(self):
            return self
        def __exit__(self, exception_type, exception_value, traceback):
            self.close()

except ImportError:
    from io import StringIO

try: # python 3.5+
    from typing import Dict, Any
    from yamlable import Y
except ImportError:
    pass

import pytest
from yaml import dump, load

from yamlable import YamlAble, yaml_info


def test_yamlable_incomplete_description():
    """ Tests that if __yaml_tag_suffix__ is not provided a YamlAble subclass cannot be declared """
    with pytest.raises(NotImplementedError) as err_info:
        class Foo(YamlAble):
            # __yaml_tag_suffix__ = 'foo'
            def __to_yaml_dict__(self):
                # type: (...) -> Dict[str, Any]
                return copy(vars(self))

            @classmethod
            def __from_yaml_dict__(cls,  # type: Type[Y]
                                   dct,  # type: Dict[str, Any]
                                   yaml_tag  # type: str
                                   ):
                # type: (...) -> Y
                return Foo(**dct)

        # instantiate
        f = Foo()

        # dump
        f.dumps_yaml()

    assert "does not seem to have a non-None '__yaml_tag_suffix__' field" in str(err_info.value)


def test_yamlable():
    """ Tests that YamlAble works correctly """

    @yaml_info(yaml_tag_ns='yaml.tests')
    class Foo(YamlAble):
        # __yaml_tag_suffix__ = 'foo'   not needed: we used @yaml_info

        def __init__(self, a, b):
            self.a = a
            self.b = b

        def __eq__(self, other):
            return vars(self) == vars(other)

        def __to_yaml_dict__(self):
            # type: (...) -> Dict[str, Any]
            return copy(vars(self))

        @classmethod
        def __from_yaml_dict__(cls,  # type: Type[Y]
                               dct,  # type: Dict[str, Any]
                               yaml_tag  # type: str
                               ):
            # type: (...) -> Y
            return Foo(**dct)

    # instantiate
    f = Foo(1, 'hello')  # note:

    # dump
    y = f.dumps_yaml(default_flow_style=False)
    assert y == """!yamlable/yaml.tests.Foo
a: 1
b: hello
"""

    # dump io
    class MemorizingStringIO(StringIO):
        """ A StringIO object that memorizes its buffer when it is closed (as opposed to the standard StringIO) """
        def close(self):
            self.value = self.getvalue()
            # super(StringIO, self).close()  # this does not work with python 2 old-style classes (StringIO is one)
            StringIO.close(self)

    s = MemorizingStringIO()
    f.dump_yaml(s, default_flow_style=False)
    assert s.value == y

    # dump pyyaml
    assert dump(f, default_flow_style=False) == y

    # load
    assert f == Foo.loads_yaml(y)

    # load io
    assert f == Foo.load_yaml(StringIO(y))

    # load pyyaml
    assert f == load(y)


def test_yamlable_legacy_method_names():
    """ Tests that YamlAbleMixIn works correctly """

    global enc
    global dec
    enc, dec = False, False

    @yaml_info(yaml_tag_ns='yaml.tests')
    class FooLegacy(YamlAble):
        # __yaml_tag_suffix__ = 'foo'   not needed: we used @yaml_info

        def __init__(self, a, b):
            self.a = a
            self.b = b

        def __eq__(self, other):
            return vars(self) == vars(other)

        def to_yaml_dict(self):
            # type: (...) -> Dict[str, Any]
            global enc
            enc = True
            return copy(vars(self))

        @classmethod
        def from_yaml_dict(cls,  # type: Type[Y]
                               dct,  # type: Dict[str, Any]
                               yaml_tag  # type: str
                               ):
            # type: (...) -> Y
            global dec
            dec = True
            return FooLegacy(**dct)

    # instantiate
    f = FooLegacy(1, 'hello')

    # dump
    y = f.dumps_yaml(default_flow_style=False)
    assert y == """!yamlable/yaml.tests.FooLegacy
a: 1
b: hello
"""

    # dump io
    class MemorizingStringIO(StringIO):
        """ A StringIO object that memorizes its buffer when it is closed (as opposed to the standard StringIO) """
        def close(self):
            self.value = self.getvalue()
            # super(StringIO, self).close()  # this does not work with python 2 old-style classes (StringIO is one)
            StringIO.close(self)

    s = MemorizingStringIO()
    f.dump_yaml(s, default_flow_style=False)
    assert s.value == y

    # dump pyyaml
    assert dump(f, default_flow_style=False) == y

    # load
    assert f == FooLegacy.loads_yaml(y)

    # load io
    assert f == FooLegacy.load_yaml(StringIO(y))

    # load pyyaml
    assert f == load(y)

    assert enc
    assert dec


# TODO override so that tag is not supported, to check error message
def test_yamlable_not_supported():

    @yaml_info(yaml_tag_ns='yaml.tests')
    class Foo_Err(YamlAble):
        # __yaml_tag_suffix__ = 'foo'   not needed: we used @yaml_info

        def __init__(self, a, b):
            self.a = a
            self.b = b

        def __eq__(self, other):
            return vars(self) == vars(other)

        def __to_yaml_dict__(self):
            # type: (...) -> Dict[str, Any]
            return copy(vars(self))

        @classmethod
        def __from_yaml_dict__(cls,  # type: Type[Y]
                               dct,  # type: Dict[str, Any]
                               yaml_tag  # type: str
                               ):
            # type: (...) -> Y
            return Foo_Err(**dct)

        @classmethod
        def is_yaml_tag_supported(cls,
                                  yaml_tag  # type: str
                                  ):
            # type: (...) -> bool
            # ALWAYS return false
            return False

    with pytest.raises(TypeError) as err_info:
        Foo_Err.loads_yaml("!yamlable/yaml.tests.Foo_Err {a: 1, b: hello}\n")

    assert "No YamlAble subclass found able to decode object" in str(err_info.value)


def test_yamlable_default_impl():
    """ tests that the default implementation works """

    @yaml_info(yaml_tag_ns='yaml.tests')
    class Foo_Default(YamlAble):
        def __init__(self, a, b):
            self.a = a
            self.b = b

    f = Foo_Default(1, 'hello')
    s = """!yamlable/yaml.tests.Foo_Default
a: 1
b: hello
"""
    assert dump(f, default_flow_style=False) == s

    assert dump(load(dump(load(s))), default_flow_style=False) == s


def test_help_yaml_info():

    @yaml_info("com.example.MyFoo")
    class Foo(YamlAble):
        pass

    assert Foo.__yaml_tag_suffix__ == "com.example.MyFoo"

    @yaml_info(yaml_tag_ns="com.example")
    class Foo(YamlAble):
        pass

    assert Foo.__yaml_tag_suffix__ == "com.example.Foo"

    assert Foo().dumps_yaml() == """!yamlable/com.example.Foo {}
"""


def test_abstract_parent_error():
    """This tests that we can define an abstract parent class with the YamlAble behaviour and inherit it"""

    class AbstractFooE(YamlAble):
        pass

    class FooError(AbstractFooE):
        """
        This class inherits from the parent without redefining a yaml tag
        """
        def __init__(self, a, b):
            self.a = a
            self.b = b

        def __eq__(self, other):
            return vars(self) == vars(other)

    # instantiate
    e = FooError(1, 'hello')

    # dump
    with pytest.raises(NotImplementedError):
        e.dumps_yaml()


def test_abstract_parent():
    """This tests that we can define an abstract parent class with the YamlAble behaviour and inherit it"""

    class AbstractFooV(YamlAble):
        pass

    @yaml_info(yaml_tag_ns='yaml.tests')
    class FooValid(AbstractFooV):

        def __init__(self, a, b):
            self.a = a
            self.b = b

        def __eq__(self, other):
            return vars(self) == vars(other)

    # instantiate
    f = FooValid(1, 'hello')  # note:

    # dump
    y = f.dumps_yaml(default_flow_style=False)
    assert y == """!yamlable/yaml.tests.FooValid
a: 1
b: hello
"""

    # dump io
    class MemorizingStringIO(StringIO):
        """ A StringIO object that memorizes its buffer when it is closed (as opposed to the standard StringIO) """

        def close(self):
            self.value = self.getvalue()
            # super(StringIO, self).close()  # this does not work with python 2 old-style classes (StringIO is one)
            StringIO.close(self)

    s = MemorizingStringIO()
    f.dump_yaml(s, default_flow_style=False)
    assert s.value == y

    # dump pyyaml
    assert dump(f, default_flow_style=False) == y

    # load
    assert f == FooValid.loads_yaml(y)

    # load io
    assert f == FooValid.load_yaml(StringIO(y))

    # load pyyaml
    assert f == load(y)

[((57, 5, 57, 40), 'yamlable.yaml_info', 'yaml_info', (), '', False, 'from yamlable import YamlAble, yaml_info\n'), ((122, 5, 122, 40), 'yamlable.yaml_info', 'yaml_info', (), '', False, 'from yamlable import YamlAble, yaml_info\n'), ((190, 5, 190, 40), 'yamlable.yaml_info', 'yaml_info', (), '', False, 'from yamlable import YamlAble, yaml_info\n'), ((230, 5, 230, 40), 'yamlable.yaml_info', 'yaml_info', (), '', False, 'from yamlable import YamlAble, yaml_info\n'), ((248, 5, 248, 35), 'yamlable.yaml_info', 'yaml_info', ({(248, 15, 248, 34): '"""com.example.MyFoo"""'}, {}), "('com.example.MyFoo')", False, 'from yamlable import YamlAble, yaml_info\n'), ((254, 5, 254, 41), 'yamlable.yaml_info', 'yaml_info', (), '', False, 'from yamlable import YamlAble, yaml_info\n'), ((295, 5, 295, 40), 'yamlable.yaml_info', 'yaml_info', (), '', False, 'from yamlable import YamlAble, yaml_info\n'), ((30, 9, 30, 43), 'pytest.raises', 'pytest.raises', ({(30, 23, 30, 42): 'NotImplementedError'}, {}), '(NotImplementedError)', False, 'import pytest\n'), ((103, 11, 103, 44), 'yaml.dump', 'dump', (), '', False, 'from yaml import dump, load\n'), ((112, 16, 112, 23), 'yaml.load', 'load', ({(112, 21, 112, 22): 'y'}, {}), '(y)', False, 'from yaml import dump, load\n'), ((172, 11, 172, 44), 'yaml.dump', 'dump', (), '', False, 'from yaml import dump, load\n'), ((181, 16, 181, 23), 'yaml.load', 'load', ({(181, 21, 181, 22): 'y'}, {}), '(y)', False, 'from yaml import dump, load\n'), ((221, 9, 221, 33), 'pytest.raises', 'pytest.raises', ({(221, 23, 221, 32): 'TypeError'}, {}), '(TypeError)', False, 'import pytest\n'), ((241, 11, 241, 44), 'yaml.dump', 'dump', (), '', False, 'from yaml import dump, load\n'), ((285, 9, 285, 43), 'pytest.raises', 'pytest.raises', ({(285, 23, 285, 42): 'NotImplementedError'}, {}), '(NotImplementedError)', False, 'import pytest\n'), ((329, 11, 329, 44), 'yaml.dump', 'dump', (), '', False, 'from yaml import dump, load\n'), ((338, 16, 338, 23), 'yaml.load', 'load', ({(338, 21, 338, 22): 'y'}, {}), '(y)', False, 'from yaml import dump, load\n'), ((96, 12, 96, 32), 'io.StringIO.close', 'StringIO.close', ({(96, 27, 96, 31): 'self'}, {}), '(self)', False, 'from io import StringIO\n'), ((109, 30, 109, 41), 'io.StringIO', 'StringIO', ({(109, 39, 109, 40): 'y'}, {}), '(y)', False, 'from io import StringIO\n'), ((165, 12, 165, 32), 'io.StringIO.close', 'StringIO.close', ({(165, 27, 165, 31): 'self'}, {}), '(self)', False, 'from io import StringIO\n'), ((178, 36, 178, 47), 'io.StringIO', 'StringIO', ({(178, 45, 178, 46): 'y'}, {}), '(y)', False, 'from io import StringIO\n'), ((322, 12, 322, 32), 'io.StringIO.close', 'StringIO.close', ({(322, 27, 322, 31): 'self'}, {}), '(self)', False, 'from io import StringIO\n'), ((335, 35, 335, 46), 'io.StringIO', 'StringIO', ({(335, 44, 335, 45): 'y'}, {}), '(y)', False, 'from io import StringIO\n'), ((243, 26, 243, 33), 'yaml.load', 'load', ({(243, 31, 243, 32): 's'}, {}), '(s)', False, 'from yaml import dump, load\n')]