import re

import pytest

from rlp_cython import SerializationError
from rlp_cython import infer_sedes, encode, decode
from rlp_cython.sedes import big_endian_int, binary, List
from rlp_cython.sedes.serializable import Serializable


class RLPType1(Serializable):
    fields = [
        ('field1', big_endian_int),
        ('field2', binary),
        ('field3', List((big_endian_int, binary)))
    ]


class RLPType2(Serializable):
    fields = [
        ('field2_1', RLPType1),
        ('field2_2', List((RLPType1, RLPType1))),
    ]


class RLPType3(Serializable):
    fields = [
        ('field1', big_endian_int),
        ('field2', big_endian_int),
        ('field3', big_endian_int),
    ]

    def __init__(self, field2, field1, field3, **kwargs):
        super().__init__(field1=field1, field2=field2, field3=field3, **kwargs)


class RLPType4(RLPType3):
    pass


_type_1_a = RLPType1(5, b'a', (0, b''))
_type_1_b = RLPType1(9, b'b', (2, b''))
_type_2 = RLPType2(_type_1_a.copy(), [_type_1_a.copy(), _type_1_b.copy()])


@pytest.fixture
def type_1_a():
    return _type_1_a.copy()


@pytest.fixture
def type_1_b():
    return _type_1_b.copy()


@pytest.fixture
def type_2():
    return _type_2.copy()


@pytest.fixture(params=[_type_1_a, _type_1_b, _type_2])
def rlp_obj(request):
    return request.param.copy()


@pytest.mark.parametrize(
    'rlptype,args,kwargs,exception_includes',
    (
        # missing fields args
        (RLPType1, [], {}, ['field1', 'field2', 'field3']),
        (RLPType1, [8], {}, ['field2', 'field3']),
        (RLPType1, [7, 8], {}, ['field3']),
        # missing fields kwargs
        (RLPType1, [], {'field1': 7}, ['field2', 'field3']),
        (RLPType1, [], {'field1': 7, 'field2': 8}, ['field3']),
        (RLPType1, [], {'field2': 7, 'field3': (1, b'')}, ['field1']),
        (RLPType1, [], {'field3': (1, b'')}, ['field1', 'field2']),
        (RLPType1, [], {'field2': 7}, ['field1', 'field3']),
        # missing fields args and kwargs
        (RLPType1, [7], {'field2': 8}, ['field3']),
        (RLPType1, [7], {'field3': (1, b'')}, ['field2']),
        # duplicate fields
        (RLPType1, [7], {'field1': 8}, ['field1']),
        (RLPType1, [7, 8], {'field1': 8, 'field2': 7}, ['field1', 'field2']),
    ),
)
def test_serializable_initialization_validation(rlptype, args, kwargs, exception_includes):
    for msg_part in exception_includes:
        with pytest.raises(TypeError, match=msg_part):
            rlptype(*args, **kwargs)


@pytest.mark.parametrize(
    'args,kwargs',
    (
        ([2, 1, 3], {}),
        ([2, 1], {'field3': 3}),
        ([2], {'field3': 3, 'field1': 1}),
        ([], {'field3': 3, 'field1': 1, 'field2': 2}),
    ),
)
def test_serializable_initialization_args_kwargs_mix(args, kwargs):
    obj = RLPType3(*args, **kwargs)

    assert obj.field1 == 1
    assert obj.field2 == 2
    assert obj.field3 == 3


@pytest.mark.parametrize(
    'lookup,expected',
    (
        (0, 5),
        (1, b'a'),
        (2, (0, b'')),
        (slice(0, 1), (5,)),
        (slice(0, 2), (5, b'a')),
        (slice(0, 3), (5, b'a', (0, b''))),
        (slice(1, 3), (b'a', (0, b''))),
        (slice(2, 3), ((0, b''),)),
        (slice(None, 3), (5, b'a', (0, b''))),
        (slice(None, 2), (5, b'a')),
        (slice(None, 1), (5,)),
        (slice(None, 0), tuple()),
        (slice(0, None), (5, b'a', (0, b''))),
        (slice(1, None), (b'a', (0, b''))),
        (slice(2, None), ((0, b''),)),
        (slice(2, None), ((0, b''),)),
        (slice(None, None), (5, b'a', (0, b''))),
        (slice(-1, None), ((0, b''),)),
        (slice(-2, None), (b'a', (0, b''),)),
        (slice(-3, None), (5, b'a', (0, b''),)),
    ),
)
def test_serializable_getitem_lookups(type_1_a, lookup, expected):
    actual = type_1_a[lookup]
    assert actual == expected


def test_serializable_subclass_retains_field_info_from_parent():
    obj = RLPType4(2, 1, 3)
    assert obj.field1 == 1
    assert obj.field2 == 2
    assert obj.field3 == 3


def test_deserialization_for_custom_init_method():
    type_3 = RLPType3(2, 1, 3)
    assert type_3.field1 == 1
    assert type_3.field2 == 2
    assert type_3.field3 == 3

    result = decode(encode(type_3), sedes=RLPType3)

    assert result.field1 == 1
    assert result.field2 == 2
    assert result.field3 == 3


def test_serializable_iterator():
    values = (5, b'a', (1, b'c'))
    obj = RLPType1(*values)
    assert tuple(obj) == values


def test_serializable_equality(type_1_a, type_1_b, type_2):
    # equality
    assert type_1_a == type_1_a
    assert type_1_a == RLPType1(*type_1_a)
    assert type_1_b == type_1_b
    assert type_1_b == RLPType1(*type_1_b)

    assert type_2 == type_2
    assert type_1_a != type_1_b
    assert type_1_b != type_2
    assert type_2 != type_1_a


def test_serializable_sedes_inference(type_1_a, type_1_b, type_2):
    assert infer_sedes(type_1_a) == RLPType1
    assert infer_sedes(type_1_b) == RLPType1
    assert infer_sedes(type_2) == RLPType2


def test_serializable_invalid_serialization_value(type_1_a, type_1_b, type_2):
    with pytest.raises(SerializationError):
        RLPType1.serialize(type_2)
    with pytest.raises(SerializationError):
        RLPType2.serialize(type_1_a)
    with pytest.raises(SerializationError):
        RLPType2.serialize(type_1_b)


def test_serializable_serialization(type_1_a, type_1_b, type_2):
    serial_1_a = RLPType1.serialize(type_1_a)
    serial_1_b = RLPType1.serialize(type_1_b)
    serial_2 = RLPType2.serialize(type_2)
    assert serial_1_a == [b'\x05', b'a', [b'', b'']]
    assert serial_1_b == [b'\x09', b'b', [b'\x02', b'']]
    assert serial_2 == [serial_1_a, [serial_1_a, serial_1_b]]


def test_serializable_deserialization(type_1_a, type_1_b, type_2):
    serial_1_a = RLPType1.serialize(type_1_a)
    serial_1_b = RLPType1.serialize(type_1_b)
    serial_2 = RLPType2.serialize(type_2)

    res_type_1_a = RLPType1.deserialize(serial_1_a)
    res_type_1_b = RLPType1.deserialize(serial_1_b)
    res_type_2 = RLPType2.deserialize(serial_2)

    assert res_type_1_a == type_1_a
    assert res_type_1_b == type_1_b
    assert res_type_2 == type_2


def test_serializable_field_immutability(type_1_a, type_1_b, type_2):
    with pytest.raises(AttributeError, match=r"can't set attribute"):
        type_1_a.field1 += 1
    assert type_1_a.field1 == 5

    with pytest.raises(AttributeError, match=r"can't set attribute"):
        type_1_a.field2 = b'x'
    assert type_1_a.field2 == b'a'

    with pytest.raises(AttributeError, match=r"can't set attribute"):
        type_2.field2_1.field1 += 1
    assert type_2.field2_1.field1 == 5

    with pytest.raises(TypeError, match=r"'tuple' object does not support item assignment"):
        type_2.field2_2[1] = type_1_a
    assert type_2.field2_2[1] == type_1_b


def test_serializable_encoding_rlp_caching(rlp_obj):
    assert rlp_obj._cached_rlp is None

    # obj should start out without a cache
    rlp_code = encode(rlp_obj, cache=False)
    assert rlp_obj._cached_rlp is None

    # cache should be populated now.
    assert encode(rlp_obj, cache=True) == rlp_code
    assert rlp_obj._cached_rlp == rlp_code

    # cache should still be populated and encoding should used cached_rlp value
    rlp_obj._cached_rlp = b'test-uses-cache'
    assert encode(rlp_obj, cache=True) == b'test-uses-cache'

    obj_decoded = decode(rlp_code, sedes=rlp_obj.__class__)
    assert obj_decoded == rlp_obj
    assert obj_decoded._cached_rlp == rlp_code


def test_list_of_serializable_decoding_rlp_caching(rlp_obj):
    rlp_obj_code = encode(rlp_obj, cache=False)
    L = [rlp_obj, rlp_obj]
    list_code = encode(L, cache=False)

    L2 = decode(list_code, sedes=List((type(rlp_obj), type(rlp_obj))), recursive_cache=True)
    assert L2[0]._cached_rlp == rlp_obj_code
    assert L2[1]._cached_rlp == rlp_obj_code


def test_serializable_basic_copy(type_1_a):
    n_type_1_a = type_1_a.copy()
    assert n_type_1_a == type_1_a
    assert n_type_1_a is not type_1_a


def test_serializable_copy_with_nested_serializables(type_2):
    n_type_2 = type_2.copy()
    assert n_type_2 == type_2
    assert n_type_2 is not type_2

    assert n_type_2.field2_1 == type_2.field2_1
    assert n_type_2.field2_1 is not type_2.field2_1

    assert n_type_2.field2_2 == type_2.field2_2
    assert all(left == right for left, right in zip(n_type_2.field2_2, type_2.field2_2))
    assert all(left is not right for left, right in zip(n_type_2.field2_2, type_2.field2_2))


def test_serializable_build_changeset(type_1_a):
    with type_1_a.build_changeset() as changeset:
        # make changes to copy
        changeset.field1 = 1234
        changeset.field2 = b'arst'

        # check that the copy has the new field values
        assert changeset.field1 == 1234
        assert changeset.field2 == b'arst'

        n_type_1_a = changeset.commit()

    # check that the copy has the new field values
    assert n_type_1_a.field1 == 1234
    assert n_type_1_a.field2 == b'arst'

    # ensure the base object hasn't changed.
    assert type_1_a.field1 == 5
    assert type_1_a.field2 == b'a'


def test_serializable_build_changeset_changeset_gets_decomissioned(type_1_a):
    with type_1_a.build_changeset() as changeset:
        changeset.field1 = 54321
        n_type_1_a = changeset.commit()

    # ensure that we also can't update the changeset
    with pytest.raises(AttributeError):
        changeset.field1 = 12345
    # ensure that we also can't read values from the changeset
    with pytest.raises(AttributeError):
        changeset.field1

    assert n_type_1_a.field1 == 54321


def test_serializable_build_changeset_with_params(type_1_a):
    with type_1_a.build_changeset(1234) as changeset:
        assert changeset.field1 == 1234

        n_type_1_a = changeset.commit()
    assert n_type_1_a.field1 == 1234


def test_serializable_build_changeset_using_open_close_api(type_1_a):
    changeset = type_1_a.build_changeset()
    changeset.open()

    # make changes to copy
    changeset.field1 = 1234
    changeset.field2 = b'arst'

    # check that the copy has the new field values
    assert changeset.field1 == 1234
    assert changeset.field2 == b'arst'

    n_type_1_a = changeset.build_rlp()

    # check that the copy has the new field values
    assert n_type_1_a.field1 == 1234
    assert n_type_1_a.field2 == b'arst'

    # ensure the base object hasn't changed.
    assert type_1_a.field1 == 5
    assert type_1_a.field2 == b'a'

    # check we can still access the unclosed changeset
    assert changeset.field1 == 1234

    changeset.close()

    with pytest.raises(AttributeError):
        assert changeset.field1 == 1234


def test_serializable_with_duplicate_field_names_is_error():
    msg1 = "duplicated in the `fields` declaration: field_a"
    with pytest.raises(TypeError, match=msg1):
        class ParentA(Serializable):
            fields = (
                ('field_a', big_endian_int),
                ('field_c', big_endian_int),
                ('field_d', big_endian_int),
                ('field_a', big_endian_int),
            )

    msg2 = "duplicated in the `fields` declaration: field_a,field_c"
    with pytest.raises(TypeError, match=msg2):
        class ParentB(Serializable):
            fields = (
                ('field_a', big_endian_int),
                ('field_c', big_endian_int),
                ('field_d', big_endian_int),
                ('field_a', big_endian_int),
                ('field_c', big_endian_int),
            )


def test_serializable_inheritance_enforces_inclusion_of_parent_fields():
    class Parent(Serializable):
        fields = (
            ('field_a', big_endian_int),
            ('field_b', big_endian_int),
            ('field_c', big_endian_int),
            ('field_d', big_endian_int),
        )

    with pytest.raises(TypeError, match="field_a,field_c"):
        class Child(Parent):
            fields = (
                ('field_b', big_endian_int),
                ('field_d', big_endian_int),
            )


def test_serializable_single_inheritance_with_no_fields():
    class Parent(Serializable):
        fields = (
            ('field_a', big_endian_int),
            ('field_b', big_endian_int),
        )

    class Child(Parent):
        pass

    parent = Parent(1, 2)
    assert parent.field_a == 1
    assert parent.field_b == 2
    assert Parent.serialize(parent) == [b'\x01', b'\x02']

    child = Child(3, 4)
    assert child.field_a == 3
    assert child.field_b == 4
    assert Child.serialize(child) == [b'\x03', b'\x04']


def test_serializable_single_inheritance_with_fields():
    class Parent(Serializable):
        fields = (
            ('field_a', big_endian_int),
            ('field_b', big_endian_int),
        )

    class Child(Parent):
        fields = (
            ('field_a', big_endian_int),
            ('field_b', big_endian_int),
            ('field_c', big_endian_int),
        )

    parent = Parent(1, 2)
    assert parent.field_a == 1
    assert parent.field_b == 2
    assert Parent.serialize(parent) == [b'\x01', b'\x02']

    with pytest.raises(TypeError):
        # ensure that the fields don't somehow leak into the parent class.
        Parent(1, 2, 3)

    child = Child(3, 4, 5)
    assert child.field_a == 3
    assert child.field_b == 4
    assert child.field_c == 5
    assert Child.serialize(child) == [b'\x03', b'\x04', b'\x05']


def test_serializable_inheritance_with_sedes_overrides():
    class Parent(Serializable):
        fields = (
            ('field_a', big_endian_int),
            ('field_b', big_endian_int),
        )

    class Child(Parent):
        fields = (
            ('field_a', binary),
            ('field_b', binary),
            ('field_c', binary),
        )

    parent = Parent(1, 2)
    assert parent.field_a == 1
    assert parent.field_b == 2
    assert Parent.serialize(parent) == [b'\x01', b'\x02']

    child = Child(b'1', b'2', b'3')
    assert child.field_a == b'1'
    assert child.field_b == b'2'
    assert child.field_c == b'3'
    assert Child.serialize(child) == [b'1', b'2', b'3']


def test_serializable_multiple_inheritance_without_fields_declaration_is_error():
    class ParentA(Serializable):
        fields = (
            ('field_a', big_endian_int),
        )

    class ParentB(Serializable):
        fields = (
            ('field_b', big_endian_int),
        )

    with pytest.raises(TypeError, match="explicit `fields` declaration"):
        class Child(ParentA, ParentB):
            pass


def test_serializable_multiple_inheritance_allowed_with_explicit_fields():
    class ParentA(Serializable):
        fields = (
            ('field_a', big_endian_int),
        )

    class ParentB(Serializable):
        fields = (
            ('field_b', big_endian_int),
        )

    # with same fields
    class ChildA(ParentA, ParentB):
        fields = (
            ('field_a', big_endian_int),
            ('field_b', big_endian_int),
        )

    # with extra fields
    class ChildB(ParentA, ParentB):
        fields = (
            ('field_a', big_endian_int),
            ('field_b', big_endian_int),
            ('field_c', big_endian_int),
        )


def test_serializable_multiple_inheritance_requires_all_parent_fields():
    class ParentA(Serializable):
        fields = (
            ('field_a', big_endian_int),
        )

    class ParentB(Serializable):
        fields = (
            ('field_b', big_endian_int),
        )

    with pytest.raises(TypeError, match="The following fields are missing: field_b"):
        class ChildA(ParentA, ParentB):
            fields = (
                ('field_a', big_endian_int),
            )

    with pytest.raises(TypeError, match="The following fields are missing: field_a"):
        class ChildB(ParentA, ParentB):
            fields = (
                ('field_b', big_endian_int),
            )

    with pytest.raises(TypeError, match="The following fields are missing: field_a,field_b"):
        class ChildC(ParentA, ParentB):
            fields = (
                ('field_c', big_endian_int),
                ('field_d', big_endian_int),
            )


@pytest.mark.parametrize(
    'name',
    (
        '0_starts_with_digit',
        ' starts_with_space',
        '$starts_with_dollar',
        'has_dollar_$_inside',
        'has spaces',
    )
)
def test_serializable_field_names_must_be_valid_identifiers(name):
    msg = "not valid python identifiers: `{0}`".format(re.escape(name))
    with pytest.raises(TypeError, match=msg):
        class Klass(Serializable):
            fields = (
                (name, big_endian_int),
            )


def test_serializable_inheritance_from_base_with_no_fields():
    """
    ensure that we can create base classes of the base `Serializable` without
    declaring fields.
    """
    class ExtendedSerializable(Serializable):
        pass

    class FurtherExtendedSerializable(ExtendedSerializable):
        pass
