from __future__ import absolute_import, division, print_function
import pickle

import pytest
from hypothesis import given
from hypothesis.strategies import booleans

import attr

from attr._compat import TYPE
from attr._make import Attribute, NOTHING
from attr.exceptions import FrozenInstanceError


@attr.s
class C1(object):
    x = attr.ib(validator=attr.validators.instance_of(int))
    y = attr.ib()


@attr.s(slots=True)
class C1Slots(object):
    x = attr.ib(validator=attr.validators.instance_of(int))
    y = attr.ib()


foo = None


@attr.s()
class C2(object):
    x = attr.ib(default=foo)
    y = attr.ib(default=attr.Factory(list))


@attr.s(slots=True)
class C2Slots(object):
    x = attr.ib(default=foo)
    y = attr.ib(default=attr.Factory(list))


@attr.s
class Super(object):
    x = attr.ib()

    def meth(self):
        return self.x


@attr.s(slots=True)
class SuperSlots(object):
    x = attr.ib()

    def meth(self):
        return self.x


@attr.s
class Sub(Super):
    y = attr.ib()


@attr.s(slots=True)
class SubSlots(SuperSlots):
    y = attr.ib()


@attr.s(frozen=True, slots=True)
class Frozen(object):
    x = attr.ib()


@attr.s(frozen=True, slots=False)
class FrozenNoSlots(object):
    x = attr.ib()


class TestDarkMagic(object):
    """
    Integration tests.
    """
    @pytest.mark.parametrize("cls", [C2, C2Slots])
    def test_fields(self, cls):
        """
        `attr.fields` works.
        """
        assert (
            Attribute(name="x", default=foo, validator=None,
                      repr=True, cmp=True, hash=True, init=True),
            Attribute(name="y", default=attr.Factory(list), validator=None,
                      repr=True, cmp=True, hash=True, init=True),
        ) == attr.fields(cls)

    @pytest.mark.parametrize("cls", [C1, C1Slots])
    def test_asdict(self, cls):
        """
        `attr.asdict` works.
        """
        assert {
            "x": 1,
            "y": 2,
        } == attr.asdict(cls(x=1, y=2))

    @pytest.mark.parametrize("cls", [C1, C1Slots])
    def test_validator(self, cls):
        """
        `instance_of` raises `TypeError` on type mismatch.
        """
        with pytest.raises(TypeError) as e:
            cls("1", 2)

        # Using C1 explicitly, since slot classes don't support this.
        assert (
            "'x' must be <{type} 'int'> (got '1' that is a <{type} "
            "'str'>).".format(type=TYPE),
            C1.x, int, "1",
        ) == e.value.args

    @given(booleans())
    def test_renaming(self, slots):
        """
        Private members are renamed but only in `__init__`.
        """
        @attr.s(slots=slots)
        class C3(object):
            _x = attr.ib()

        assert "C3(_x=1)" == repr(C3(x=1))

    @given(booleans(), booleans())
    def test_programmatic(self, slots, frozen):
        """
        `attr.make_class` works.
        """
        PC = attr.make_class("PC", ["a", "b"], slots=slots, frozen=frozen)
        assert (
            Attribute(name="a", default=NOTHING, validator=None,
                      repr=True, cmp=True, hash=True, init=True),
            Attribute(name="b", default=NOTHING, validator=None,
                      repr=True, cmp=True, hash=True, init=True),
        ) == attr.fields(PC)

    @pytest.mark.parametrize("cls", [Sub, SubSlots])
    def test_subclassing_with_extra_attrs(self, cls):
        """
        Sub-classing (where the subclass has extra attrs) does what you'd hope
        for.
        """
        obj = object()
        i = cls(x=obj, y=2)
        assert i.x is i.meth() is obj
        assert i.y == 2
        if cls is Sub:
            assert "Sub(x={obj}, y=2)".format(obj=obj) == repr(i)
        else:
            assert "SubSlots(x={obj}, y=2)".format(obj=obj) == repr(i)

    @pytest.mark.parametrize("base", [Super, SuperSlots])
    def test_subclass_without_extra_attrs(self, base):
        """
        Sub-classing (where the subclass does not have extra attrs) still
        behaves the same as a subclss with extra attrs.
        """
        class Sub2(base):
            pass

        obj = object()
        i = Sub2(x=obj)
        assert i.x is i.meth() is obj
        assert "Sub2(x={obj})".format(obj=obj) == repr(i)

    @pytest.mark.parametrize("frozen_class", [
        Frozen,  # has slots=True
        attr.make_class("FrozenToo", ["x"], slots=False, frozen=True),
    ])
    def test_frozen_instance(self, frozen_class):
        """
        Frozen instances can't be modified (easily).
        """
        frozen = frozen_class(1)

        with pytest.raises(FrozenInstanceError) as e:
            frozen.x = 2

        assert e.value.args[0] == "can't set attribute"
        assert 1 == frozen.x

    @pytest.mark.parametrize("cls",
                             [C1, C1Slots, C2, C2Slots, Super, SuperSlots,
                              Sub, SubSlots, Frozen, FrozenNoSlots])
    @pytest.mark.parametrize("protocol",
                             range(2, pickle.HIGHEST_PROTOCOL + 1))
    def test_pickle_attributes(self, cls, protocol):
        """
        Pickling/un-pickling of Attribute instances works.
        """
        for attribute in attr.fields(cls):
            assert attribute == pickle.loads(pickle.dumps(attribute, protocol))

    @pytest.mark.parametrize("cls",
                             [C1, C1Slots, C2, C2Slots, Super, SuperSlots,
                              Sub, SubSlots, Frozen, FrozenNoSlots])
    @pytest.mark.parametrize("protocol",
                             range(2, pickle.HIGHEST_PROTOCOL + 1))
    def test_pickle_object(self, cls, protocol):
        """
        Pickle object serialization works on all kinds of attrs classes.
        """
        if len(attr.fields(cls)) == 2:
            obj = cls(123, 456)
        else:
            obj = cls(123)
        assert repr(obj) == repr(pickle.loads(pickle.dumps(obj, protocol)))
