import pickle
import datetime
import pytest
import uuid
from Aspidites._vendor.pyrsistent import (
    PRecord,
    field,
    InvariantException,
    ny,
    pset,
    PSet,
    CheckedPVector,
    PTypeError,
    pset_field,
    pvector_field,
    pmap_field,
    pmap,
    PMap,
    pvector,
    PVector,
    v,
    m,
)


class ARecord(PRecord):
    x = field(type=(int, float))
    y = field()


class Hierarchy(PRecord):
    point1 = field(ARecord)
    point2 = field(ARecord)
    points = pvector_field(ARecord)


class RecordContainingContainers(PRecord):
    map = pmap_field(str, str)
    vec = pvector_field(str)
    set = pset_field(str)


class UniqueThing(PRecord):
    id = field(type=uuid.UUID, factory=uuid.UUID)


class Something(object):
    pass


class Another(object):
    pass


def test_create_ignore_extra_true():
    h = Hierarchy.create(
        {
            "point1": {"x": 1, "y": "foo", "extra_field_0": "extra_data_0"},
            "point2": {"x": 1, "y": "foo", "extra_field_1": "extra_data_1"},
            "extra_field_2": "extra_data_2",
        },
        ignore_extra=True,
    )
    assert h


def test_create_ignore_extra_true_sequence_hierarchy():
    h = Hierarchy.create(
        {
            "point1": {"x": 1, "y": "foo", "extra_field_0": "extra_data_0"},
            "point2": {"x": 1, "y": "foo", "extra_field_1": "extra_data_1"},
            "points": [
                {"x": 1, "y": "foo", "extra_field_2": "extra_data_2"},
                {"x": 1, "y": "foo", "extra_field_3": "extra_data_3"},
            ],
            "extra_field____": "extra_data_2",
        },
        ignore_extra=True,
    )
    assert h


def test_ignore_extra_for_pvector_field():
    class HierarchyA(PRecord):
        points = pvector_field(ARecord, optional=False)

    class HierarchyB(PRecord):
        points = pvector_field(ARecord, optional=True)

    point_object = {"x": 1, "y": "foo", "extra_field": 69}

    h = HierarchyA.create({"points": [point_object]}, ignore_extra=True)
    assert h
    h = HierarchyB.create({"points": [point_object]}, ignore_extra=True)
    assert h


def test_create():
    r = ARecord(x=1, y="foo")
    assert r.x == 1
    assert r.y == "foo"
    assert isinstance(r, ARecord)


def test_create_ignore_extra():
    r = ARecord.create({"x": 1, "y": "foo", "z": None}, ignore_extra=True)
    assert r.x == 1
    assert r.y == "foo"
    assert isinstance(r, ARecord)


def test_create_ignore_extra_false():
    with pytest.raises(AttributeError):
        _ = ARecord.create({"x": 1, "y": "foo", "z": None})


def test_correct_assignment():
    r = ARecord(x=1, y="foo")
    r2 = r.set("x", 2.0)
    r3 = r2.set("y", "bar")

    assert r2 == {"x": 2.0, "y": "foo"}
    assert r3 == {"x": 2.0, "y": "bar"}
    assert isinstance(r3, ARecord)


def test_direct_assignment_not_possible():
    with pytest.raises(AttributeError):
        ARecord().x = 1


def test_cannot_assign_undeclared_fields():
    with pytest.raises(AttributeError):
        ARecord().set("z", 5)


def test_cannot_assign_wrong_type_to_fields():
    try:
        ARecord().set("x", "foo")
        assert False
    except PTypeError as e:
        assert e.source_class == ARecord
        assert e.field == "x"
        assert e.expected_types == set([int, float])
        assert e.actual_type is type("foo")


def test_cannot_construct_with_undeclared_fields():
    with pytest.raises(AttributeError):
        ARecord(z=5)


def test_cannot_construct_with_fields_of_wrong_type():
    with pytest.raises(TypeError):
        ARecord(x="foo")


def test_support_record_inheritance():
    class BRecord(ARecord):
        z = field()

    r = BRecord(x=1, y="foo", z="bar")

    assert isinstance(r, BRecord)
    assert isinstance(r, ARecord)
    assert r == {"x": 1, "y": "foo", "z": "bar"}


def test_single_type_spec():
    class A(PRecord):
        x = field(type=int)

    r = A(x=1)
    assert r.x == 1

    with pytest.raises(TypeError):
        r.set("x", "foo")


def test_remove():
    r = ARecord(x=1, y="foo")
    r2 = r.remove("y")

    assert isinstance(r2, ARecord)
    assert r2 == {"x": 1}


def test_remove_non_existing_member():
    r = ARecord(x=1, y="foo")

    with pytest.raises(KeyError):
        r.remove("z")


def test_field_invariant_must_hold():
    class BRecord(PRecord):
        x = field(invariant=lambda x: (x > 1, "x too small"))
        y = field(mandatory=True)

    try:
        BRecord(x=1)
        assert False
    except InvariantException as e:
        assert e.invariant_errors == ("x too small",)
        assert e.missing_fields == ("BRecord.y",)


def test_global_invariant_must_hold():
    class BRecord(PRecord):
        __invariant__ = lambda r: (r.x <= r.y, "y smaller than x")
        x = field()
        y = field()

    BRecord(x=1, y=2)

    try:
        BRecord(x=2, y=1)
        assert False
    except InvariantException as e:
        assert e.invariant_errors == ("y smaller than x",)
        assert e.missing_fields == ()


def test_set_multiple_fields():
    a = ARecord(x=1, y="foo")
    b = a.set(x=2, y="bar")

    assert b == {"x": 2, "y": "bar"}


def test_initial_value():
    class BRecord(PRecord):
        x = field(initial=1)
        y = field(initial=2)

    a = BRecord()
    assert a.x == 1
    assert a.y == 2


def test_enum_field():
    try:
        from enum import Enum
    except ImportError:
        return  # Enum not supported in this environment

    class ExampleEnum(Enum):
        x = 1
        y = 2

    class RecordContainingEnum(PRecord):
        enum_field = field(type=ExampleEnum)

    r = RecordContainingEnum(enum_field=ExampleEnum.x)
    assert r.enum_field == ExampleEnum.x


def test_type_specification_must_be_a_type():
    with pytest.raises(TypeError):

        class BRecord(PRecord):
            x = field(type=1)


def test_initial_must_be_of_correct_type():
    with pytest.raises(TypeError):

        class BRecord(PRecord):
            x = field(type=int, initial="foo")


def test_invariant_must_be_callable():
    with pytest.raises(TypeError):

        class BRecord(PRecord):
            x = field(invariant="foo")  # type: ignore


def test_global_invariants_are_inherited():
    class BRecord(PRecord):
        __invariant__ = lambda r: (r.x % r.y == 0, "modulo")
        x = field()
        y = field()

    class CRecord(BRecord):
        __invariant__ = lambda r: (r.x > r.y, "size")

    try:
        CRecord(x=5, y=3)
        assert False
    except InvariantException as e:
        assert e.invariant_errors == ("modulo",)


def test_global_invariants_must_be_callable():
    with pytest.raises(TypeError):

        class CRecord(PRecord):
            __invariant__ = 1


def test_repr():
    r = ARecord(x=1, y=2)
    assert repr(r) == "ARecord(x=1, y=2)" or repr(r) == "ARecord(y=2, x=1)"


def test_factory():
    class BRecord(PRecord):
        x = field(type=int, factory=int)

    assert BRecord(x=2.5) == {"x": 2}


def test_factory_must_be_callable():
    with pytest.raises(TypeError):

        class BRecord(PRecord):
            x = field(type=int, factory=1)  # type: ignore


def test_nested_record_construction():
    class BRecord(PRecord):
        x = field(int, factory=int)

    class CRecord(PRecord):
        a = field()
        b = field(type=BRecord)

    r = CRecord.create({"a": "foo", "b": {"x": "5"}})
    assert isinstance(r, CRecord)
    assert isinstance(r.b, BRecord)
    assert r == {"a": "foo", "b": {"x": 5}}


def test_pickling():
    x = ARecord(x=2.0, y="bar")
    y = pickle.loads(pickle.dumps(x, -1))

    assert x == y
    assert isinstance(y, ARecord)


def test_supports_pickling_with_typed_container_fields():
    obj = RecordContainingContainers(
        map={"foo": "bar"}, set=["hello", "there"], vec=["a", "b"]
    )
    obj2 = pickle.loads(pickle.dumps(obj))
    assert obj == obj2


def test_all_invariant_errors_reported():
    class BRecord(PRecord):
        x = field(factory=int, invariant=lambda x: (x >= 0, "x negative"))
        y = field(mandatory=True)

    class CRecord(PRecord):
        a = field(invariant=lambda x: (x != 0, "a zero"))
        b = field(type=BRecord)

    try:
        CRecord.create({"a": 0, "b": {"x": -5}})
        assert False
    except InvariantException as e:
        assert set(e.invariant_errors) == set(["x negative", "a zero"])
        assert e.missing_fields == ("BRecord.y",)


def test_precord_factory_method_is_idempotent():
    class BRecord(PRecord):
        x = field()
        y = field()

    r = BRecord(x=1, y=2)
    assert BRecord.create(r) is r


def test_serialize():
    class BRecord(PRecord):
        d = field(
            type=datetime.date,
            factory=lambda d: datetime.datetime.strptime(d, "%d%m%Y").date(),
            serializer=lambda format, d: d.strftime("%Y-%m-%d")
            if format == "ISO"
            else d.strftime("%d%m%Y"),
        )

    assert BRecord(d="14012015").serialize("ISO") == {"d": "2015-01-14"}
    assert BRecord(d="14012015").serialize("other") == {"d": "14012015"}


def test_nested_serialize():
    class BRecord(PRecord):
        d = field(serializer=lambda format, d: format)

    class CRecord(PRecord):
        b = field()

    serialized = CRecord(b=BRecord(d="foo")).serialize("bar")

    assert serialized == {"b": {"d": "bar"}}
    assert isinstance(serialized, dict)


def test_serializer_must_be_callable():
    with pytest.raises(TypeError):

        class CRecord(PRecord):
            x = field(serializer=1)  # type: ignore


def test_transform_without_update_returns_same_precord():
    r = ARecord(x=2.0, y="bar")
    assert r.transform([ny], lambda x: x) is r


class Application(PRecord):
    name = field(type=str)
    image = field(type=str)


class ApplicationVector(CheckedPVector):
    __type__ = Application


class Node(PRecord):
    applications = field(type=ApplicationVector)


def test_nested_create_serialize():
    node = Node(
        applications=[
            Application(name="myapp", image="myimage"),
            Application(name="b", image="c"),
        ]
    )

    node2 = Node.create(
        {
            "applications": [
                {"name": "myapp", "image": "myimage"},
                {"name": "b", "image": "c"},
            ]
        }
    )

    assert node == node2

    serialized = node.serialize()
    restored = Node.create(serialized)

    assert restored == node


def test_pset_field_initial_value():
    """
    ``pset_field`` results in initial value that is empty.
    """

    class Record(PRecord):
        value = pset_field(int)

    assert Record() == Record(value=[])


def test_pset_field_custom_initial():
    """
    A custom initial value can be passed in.
    """

    class Record(PRecord):
        value = pset_field(int, initial=(1, 2))

    assert Record() == Record(value=[1, 2])


def test_pset_field_factory():
    """
    ``pset_field`` has a factory that creates a ``PSet``.
    """

    class Record(PRecord):
        value = pset_field(int)

    record = Record(value=[1, 2])
    assert isinstance(record.value, PSet)


def test_pset_field_checked_set():
    """
    ``pset_field`` results in a set that enforces its type.
    """

    class Record(PRecord):
        value = pset_field(int)

    record = Record(value=[1, 2])
    with pytest.raises(TypeError):
        record.value.add("hello")  # type: ignore


def test_pset_field_checked_vector_multiple_types():
    """
    ``pset_field`` results in a vector that enforces its types.
    """

    class Record(PRecord):
        value = pset_field((int, str))

    record = Record(value=[1, 2, "hello"])
    with pytest.raises(TypeError):
        record.value.add(object())


def test_pset_field_type():
    """
    ``pset_field`` enforces its type.
    """

    class Record(PRecord):
        value = pset_field(int)

    record = Record()
    with pytest.raises(TypeError):
        record.set("value", None)


def test_pset_field_mandatory():
    """
    ``pset_field`` is a mandatory field.
    """

    class Record(PRecord):
        value = pset_field(int)

    record = Record(value=[1])
    with pytest.raises(InvariantException):
        record.remove("value")


def test_pset_field_default_non_optional():
    """
    By default ``pset_field`` is non-optional, i.e. does not allow
    ``None``.
    """

    class Record(PRecord):
        value = pset_field(int)

    with pytest.raises(TypeError):
        Record(value=None)


def test_pset_field_explicit_non_optional():
    """
    If ``optional`` argument is ``False`` then ``pset_field`` is
    non-optional, i.e. does not allow ``None``.
    """

    class Record(PRecord):
        value = pset_field(int, optional=False)

    with pytest.raises(TypeError):
        Record(value=None)


def test_pset_field_optional():
    """
    If ``optional`` argument is true, ``None`` is acceptable alternative
    to a set.
    """

    class Record(PRecord):
        value = pset_field(int, optional=True)

    assert (Record(value=[1, 2]).value, Record(value=None).value) == (
        pset([1, 2]),
        None,
    )


def test_pset_field_name():
    """
    The created set class name is based on the type of items in the set.
    """

    class Record(PRecord):
        value = pset_field(Something)
        value2 = pset_field(int)

    assert (Record().value.__class__.__name__, Record().value2.__class__.__name__) == (
        "SomethingPSet",
        "IntPSet",
    )


def test_pset_multiple_types_field_name():
    """
    The created set class name is based on the multiple given types of
    items in the set.
    """

    class Record(PRecord):
        value = pset_field((Something, int))

    assert Record().value.__class__.__name__ == "SomethingIntPSet"


def test_pset_field_name_string_type():
    """
    The created set class name is based on the type of items specified by name
    """

    class Record(PRecord):
        value = pset_field("record_test.Something")

    assert Record().value.__class__.__name__ == "SomethingPSet"


def test_pset_multiple_string_types_field_name():
    """
    The created set class name is based on the multiple given types of
    items in the set specified by name
    """

    class Record(PRecord):
        value = pset_field(("record_test.Something", "record_test.Another"))

    assert Record().value.__class__.__name__ == "SomethingAnotherPSet"


def test_pvector_field_initial_value():
    """
    ``pvector_field`` results in initial value that is empty.
    """

    class Record(PRecord):
        value = pvector_field(int)

    assert Record() == Record(value=[])


def test_pvector_field_custom_initial():
    """
    A custom initial value can be passed in.
    """

    class Record(PRecord):
        value = pvector_field(int, initial=(1, 2))

    assert Record() == Record(value=[1, 2])


def test_pvector_field_factory():
    """
    ``pvector_field`` has a factory that creates a ``PVector``.
    """

    class Record(PRecord):
        value = pvector_field(int)

    record = Record(value=[1, 2])
    assert isinstance(record.value, PVector)


def test_pvector_field_checked_vector():
    """
    ``pvector_field`` results in a vector that enforces its type.
    """

    class Record(PRecord):
        value = pvector_field(int)

    record = Record(value=[1, 2])
    with pytest.raises(TypeError):
        record.value.append("hello")  # type: ignore


def test_pvector_field_checked_vector_multiple_types():
    """
    ``pvector_field`` results in a vector that enforces its types.
    """

    class Record(PRecord):
        value = pvector_field((int, str))

    record = Record(value=[1, 2, "hello"])
    with pytest.raises(TypeError):
        record.value.append(object())


def test_pvector_field_type():
    """
    ``pvector_field`` enforces its type.
    """

    class Record(PRecord):
        value = pvector_field(int)

    record = Record()
    with pytest.raises(TypeError):
        record.set("value", None)


def test_pvector_field_mandatory():
    """
    ``pvector_field`` is a mandatory field.
    """

    class Record(PRecord):
        value = pvector_field(int)

    record = Record(value=[1])
    with pytest.raises(InvariantException):
        record.remove("value")


def test_pvector_field_default_non_optional():
    """
    By default ``pvector_field`` is non-optional, i.e. does not allow
    ``None``.
    """

    class Record(PRecord):
        value = pvector_field(int)

    with pytest.raises(TypeError):
        Record(value=None)


def test_pvector_field_explicit_non_optional():
    """
    If ``optional`` argument is ``False`` then ``pvector_field`` is
    non-optional, i.e. does not allow ``None``.
    """

    class Record(PRecord):
        value = pvector_field(int, optional=False)

    with pytest.raises(TypeError):
        Record(value=None)


def test_pvector_field_optional():
    """
    If ``optional`` argument is true, ``None`` is acceptable alternative
    to a sequence.
    """

    class Record(PRecord):
        value = pvector_field(int, optional=True)

    assert (Record(value=[1, 2]).value, Record(value=None).value) == (
        pvector([1, 2]),
        None,
    )


def test_pvector_field_name():
    """
    The created set class name is based on the type of items in the set.
    """

    class Record(PRecord):
        value = pvector_field(Something)
        value2 = pvector_field(int)

    assert (Record().value.__class__.__name__, Record().value2.__class__.__name__) == (
        "SomethingPVector",
        "IntPVector",
    )


def test_pvector_multiple_types_field_name():
    """
    The created vector class name is based on the multiple given types of
    items in the vector.
    """

    class Record(PRecord):
        value = pvector_field((Something, int))

    assert Record().value.__class__.__name__ == "SomethingIntPVector"


def test_pvector_field_name_string_type():
    """
    The created set class name is based on the type of items in the set
    specified by name.
    """

    class Record(PRecord):
        value = pvector_field("record_test.Something")

    assert Record().value.__class__.__name__ == "SomethingPVector"


def test_pvector_multiple_string_types_field_name():
    """
    The created vector class name is based on the multiple given types of
    items in the vector.
    """

    class Record(PRecord):
        value = pvector_field(("record_test.Something", "record_test.Another"))

    assert Record().value.__class__.__name__ == "SomethingAnotherPVector"


def test_pvector_field_create_from_nested_serialized_data():
    class Foo(PRecord):
        foo = field(type=str)

    class Bar(PRecord):
        bar = pvector_field(Foo)

    data = Bar(bar=v(Foo(foo="foo")))
    Bar.create(data.serialize()) == data


def test_pmap_field_initial_value():
    """
    ``pmap_field`` results in initial value that is empty.
    """

    class Record(PRecord):
        value = pmap_field(int, int)

    assert Record() == Record(value={})


def test_pmap_field_factory():
    """
    ``pmap_field`` has a factory that creates a ``PMap``.
    """

    class Record(PRecord):
        value = pmap_field(int, int)

    record = Record(value={1: 1234})
    assert isinstance(record.value, PMap)


def test_pmap_field_checked_map_key():
    """
    ``pmap_field`` results in a map that enforces its key type.
    """

    class Record(PRecord):
        value = pmap_field(int, type(None))

    record = Record(value={1: None})
    with pytest.raises(TypeError):
        record.value.set("hello", None)  # type: ignore


def test_pmap_field_checked_map_value():
    """
    ``pmap_field`` results in a map that enforces its value type.
    """

    class Record(PRecord):
        value = pmap_field(int, type(None))

    record = Record(value={1: None})
    with pytest.raises(TypeError):
        record.value.set(2, 4)  # type: ignore


def test_pmap_field_checked_map_key_multiple_types():
    """
    ``pmap_field`` results in a map that enforces its key types.
    """

    class Record(PRecord):
        value = pmap_field((int, str), type(None))

    record = Record(value={1: None, "hello": None})
    with pytest.raises(TypeError):
        record.value.set(object(), None)


def test_pmap_field_checked_map_value_multiple_types():
    """
    ``pmap_field`` results in a map that enforces its value types.
    """

    class Record(PRecord):
        value = pmap_field(int, (str, type(None)))

    record = Record(value={1: None, 3: "hello"})
    with pytest.raises(TypeError):
        record.value.set(2, 4)


def test_pmap_field_mandatory():
    """
    ``pmap_field`` is a mandatory field.
    """

    class Record(PRecord):
        value = pmap_field(int, int)

    record = Record()
    with pytest.raises(InvariantException):
        record.remove("value")


def test_pmap_field_default_non_optional():
    """
    By default ``pmap_field`` is non-optional, i.e. does not allow
    ``None``.
    """

    class Record(PRecord):
        value = pmap_field(int, int)

    # Ought to be TypeError, but pyrsistent doesn't quite allow that:
    with pytest.raises(AttributeError):
        Record(value=None)


def test_pmap_field_explicit_non_optional():
    """
    If ``optional`` argument is ``False`` then ``pmap_field`` is
    non-optional, i.e. does not allow ``None``.
    """

    class Record(PRecord):
        value = pmap_field(int, int, optional=False)

    # Ought to be TypeError, but pyrsistent doesn't quite allow that:
    with pytest.raises(AttributeError):
        Record(value=None)


def test_pmap_field_optional():
    """
    If ``optional`` argument is true, ``None`` is acceptable alternative
    to a set.
    """

    class Record(PRecord):
        value = pmap_field(int, int, optional=True)

    assert (Record(value={1: 2}).value, Record(value=None).value) == (
        pmap({1: 2}),
        None,
    )


def test_pmap_field_name():
    """
    The created map class name is based on the types of items in the map.
    """

    class Record(PRecord):
        value = pmap_field(Something, Another)
        value2 = pmap_field(int, float)

    assert (Record().value.__class__.__name__, Record().value2.__class__.__name__) == (
        "SomethingToAnotherPMap",
        "IntToFloatPMap",
    )


def test_pmap_field_name_multiple_types():
    """
    The created map class name is based on the types of items in the map,
    including when there are multiple supported types.
    """

    class Record(PRecord):
        value = pmap_field((Something, Another), int)
        value2 = pmap_field(str, (int, float))

    assert (Record().value.__class__.__name__, Record().value2.__class__.__name__) == (
        "SomethingAnotherToIntPMap",
        "StrToIntFloatPMap",
    )


def test_pmap_field_name_string_type():
    """
    The created map class name is based on the types of items in the map
    specified by name.
    """

    class Record(PRecord):
        value = pmap_field("record_test.Something", "record_test.Another")

    assert Record().value.__class__.__name__ == "SomethingToAnotherPMap"


def test_pmap_field_name_multiple_string_types():
    """
    The created map class name is based on the types of items in the map,
    including when there are multiple supported types.
    """

    class Record(PRecord):
        value = pmap_field(("record_test.Something", "record_test.Another"), int)
        value2 = pmap_field(str, ("record_test.Something", "record_test.Another"))

    assert (Record().value.__class__.__name__, Record().value2.__class__.__name__) == (
        "SomethingAnotherToIntPMap",
        "StrToSomethingAnotherPMap",
    )


def test_pmap_field_invariant():
    """
    The ``invariant`` parameter is passed through to ``field``.
    """

    class Record(PRecord):
        value = pmap_field(
            int,
            int,
            invariant=(lambda pmap: (len(pmap) == 1, "Exactly one item required.")),
        )

    with pytest.raises(InvariantException):
        Record(value={})
    with pytest.raises(InvariantException):
        Record(value={1: 2, 3: 4})
    assert Record(value={1: 2}).value == {1: 2}


def test_pmap_field_create_from_nested_serialized_data():
    class Foo(PRecord):
        foo = field(type=str)

    class Bar(PRecord):
        bar = pmap_field(str, Foo)

    data = Bar(bar=m(foo_key=Foo(foo="foo")))
    Bar.create(data.serialize()) == data


def test_supports_weakref():
    import weakref

    weakref.ref(ARecord(x=1, y=2))


def test_supports_lazy_initial_value_for_field():
    class MyRecord(PRecord):
        a = field(int, initial=lambda: 2)

    assert MyRecord() == MyRecord(a=2)


def test_pickle_with_one_way_factory():
    """
    A field factory isn't called when restoring from pickle.
    """
    thing = UniqueThing(id="25544626-86da-4bce-b6b6-9186c0804d64")
    assert thing == pickle.loads(pickle.dumps(thing))
