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)

[((1534, 1569), 'yamlable.yaml_info', 'yaml_info', ([], {'yaml_tag_ns': '"""yaml.tests"""'}), "(yaml_tag_ns='yaml.tests')\n", (1543, 1569), False, 'from yamlable import YamlAble, yaml_info\n'), ((3261, 3296), 'yamlable.yaml_info', 'yaml_info', ([], {'yaml_tag_ns': '"""yaml.tests"""'}), "(yaml_tag_ns='yaml.tests')\n", (3270, 3296), False, 'from yamlable import YamlAble, yaml_info\n'), ((5081, 5116), 'yamlable.yaml_info', 'yaml_info', ([], {'yaml_tag_ns': '"""yaml.tests"""'}), "(yaml_tag_ns='yaml.tests')\n", (5090, 5116), False, 'from yamlable import YamlAble, yaml_info\n'), ((6338, 6373), 'yamlable.yaml_info', 'yaml_info', ([], {'yaml_tag_ns': '"""yaml.tests"""'}), "(yaml_tag_ns='yaml.tests')\n", (6347, 6373), False, 'from yamlable import YamlAble, yaml_info\n'), ((6736, 6766), 'yamlable.yaml_info', 'yaml_info', (['"""com.example.MyFoo"""'], {}), "('com.example.MyFoo')\n", (6745, 6766), False, 'from yamlable import YamlAble, yaml_info\n'), ((6870, 6906), 'yamlable.yaml_info', 'yaml_info', ([], {'yaml_tag_ns': '"""com.example"""'}), "(yaml_tag_ns='com.example')\n", (6879, 6906), False, 'from yamlable import YamlAble, yaml_info\n'), ((7876, 7911), 'yamlable.yaml_info', 'yaml_info', ([], {'yaml_tag_ns': '"""yaml.tests"""'}), "(yaml_tag_ns='yaml.tests')\n", (7885, 7911), False, 'from yamlable import YamlAble, yaml_info\n'), ((721, 755), 'pytest.raises', 'pytest.raises', (['NotImplementedError'], {}), '(NotImplementedError)\n', (734, 755), False, 'import pytest\n'), ((2914, 2947), 'yaml.dump', 'dump', (['f'], {'default_flow_style': '(False)'}), '(f, default_flow_style=False)\n', (2918, 2947), False, 'from yaml import dump, load\n'), ((3092, 3099), 'yaml.load', 'load', (['y'], {}), '(y)\n', (3096, 3099), False, 'from yaml import dump, load\n'), ((4740, 4773), 'yaml.dump', 'dump', (['f'], {'default_flow_style': '(False)'}), '(f, default_flow_style=False)\n', (4744, 4773), False, 'from yaml import dump, load\n'), ((4930, 4937), 'yaml.load', 'load', (['y'], {}), '(y)\n', (4934, 4937), False, 'from yaml import dump, load\n'), ((6038, 6062), 'pytest.raises', 'pytest.raises', (['TypeError'], {}), '(TypeError)\n', (6051, 6062), False, 'import pytest\n'), ((6593, 6626), 'yaml.dump', 'dump', (['f'], {'default_flow_style': '(False)'}), '(f, default_flow_style=False)\n', (6597, 6626), False, 'from yaml import dump, load\n'), ((7625, 7659), 'pytest.raises', 'pytest.raises', (['NotImplementedError'], {}), '(NotImplementedError)\n', (7638, 7659), False, 'import pytest\n'), ((8805, 8838), 'yaml.dump', 'dump', (['f'], {'default_flow_style': '(False)'}), '(f, default_flow_style=False)\n', (8809, 8838), False, 'from yaml import dump, load\n'), ((8993, 9000), 'yaml.load', 'load', (['y'], {}), '(y)\n', (8997, 9000), False, 'from yaml import dump, load\n'), ((2764, 2784), 'io.StringIO.close', 'StringIO.close', (['self'], {}), '(self)\n', (2778, 2784), False, 'from io import StringIO\n'), ((3044, 3055), 'io.StringIO', 'StringIO', (['y'], {}), '(y)\n', (3052, 3055), False, 'from io import StringIO\n'), ((4590, 4610), 'io.StringIO.close', 'StringIO.close', (['self'], {}), '(self)\n', (4604, 4610), False, 'from io import StringIO\n'), ((4882, 4893), 'io.StringIO', 'StringIO', (['y'], {}), '(y)\n', (4890, 4893), False, 'from io import StringIO\n'), ((8655, 8675), 'io.StringIO.close', 'StringIO.close', (['self'], {}), '(self)\n', (8669, 8675), False, 'from io import StringIO\n'), ((8945, 8956), 'io.StringIO', 'StringIO', (['y'], {}), '(y)\n', (8953, 8956), False, 'from io import StringIO\n'), ((6659, 6666), 'yaml.load', 'load', (['s'], {}), '(s)\n', (6663, 6666), False, 'from yaml import dump, load\n')]