# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
#
# Copyright (c) 2019 Digi International Inc. All Rights Reserved.

import unittest

import six
from suitcase.crc import crc16_ccitt, crc32
from suitcase.exceptions import SuitcaseProgrammingError, SuitcasePackStructException, SuitcasePackException, \
    SuitcaseParseError, SuitcaseException
from suitcase.fields import DependentField, LengthField, VariableRawPayload, \
    Magic, BitField, BitBool, BitNum, DispatchTarget, CRCField, Payload, \
    UBInt8, UBInt16, UBInt24, UBInt32, UBInt40, UBInt48, UBInt56, UBInt64, \
    SBInt8, SBInt16, SBInt24, SBInt32, SBInt40, SBInt48, SBInt56, SBInt64, \
    ULInt8, ULInt16, ULInt24, ULInt32, ULInt40, ULInt48, ULInt56, ULInt64, \
    SLInt8, SLInt16, SLInt24, SLInt32, SLInt40, SLInt48, SLInt56, SLInt64, \
    ConditionalField, UBInt8Sequence, SBInt8Sequence, FieldProperty, \
    DispatchField, FieldArray, TypeField, SubstructureField
from suitcase.structure import Structure
import struct


def raise_value_error(*args, **kwargs):
    raise ValueError("Artifically Raised ValueError")


class SuperChild(Structure):
    options = DependentField('options')
    ubseq = DependentField('ubseq')
    length = LengthField(DependentField('submessage_length'))

    remaining = VariableRawPayload(length)


# See https://github.com/digidotcom/python-suitcase/issues/18
class BGAPIFrame(Structure):
    head = LengthField(
            BitField(16,
                     is_event=BitBool(),
                     technology_type=BitNum(4),
                     length=BitNum(11)),
            get_length=lambda f: f.length,
            set_length=lambda f, l: setattr(f, 'length', l))
    class_id = UBInt8()
    command_id = UBInt8()
    payload = Payload(head)


class TestCustomSetGetLengthField(unittest.TestCase):

    def test_unpack(self):
        frame = BGAPIFrame.from_data(b'\x00\x00\x00\x01')
        self.assertEqual(frame.head.is_event, False)
        self.assertEqual(frame.head.technology_type, 0)
        self.assertEqual(frame.head.length, 0)
        self.assertEqual(frame.class_id, 0)
        self.assertEqual(frame.command_id, 1)
        self.assertEqual(frame.payload, b"")

    def test_pack(self):
        frame = BGAPIFrame()
        frame.head.is_event = True
        frame.head.technology_type = 0b1101
        frame.class_id = 0x10
        frame.command_id = 0xFE
        frame.payload = b"Hello, world!"
        self.assertEqual(frame.pack(), b'\xe8\r\x10\xfeHello, world!')
        frame_readback = BGAPIFrame.from_data(frame.pack())
        self.assertEqual(frame_readback.head.length, len(frame.payload))


# Message containing every field
class SuperMessage(Structure):
    magic = Magic(b'\xAA\xAA')

    # bitfield
    options = BitField(8,
                       b1=BitBool(),
                       b2=BitBool(),
                       rest=BitNum(6))

    # unsigned big endian
    ubint8 = UBInt8()
    ubint16 = UBInt16()
    ubint24 = UBInt24()
    ubint32 = UBInt32()
    ubint40 = UBInt40()
    ubint48 = UBInt48()
    ubint56 = UBInt56()
    ubint64 = UBInt64()

    # signed big endian
    sbint8 = SBInt8()
    sbint16 = SBInt16()
    sbint24 = SBInt24()
    sbint32 = SBInt32()
    sbint40 = SBInt40()
    sbint48 = SBInt48()
    sbint56 = SBInt56()
    sbint64 = SBInt64()

    # unsigned little endian
    ulint8 = ULInt8()
    ulint16 = ULInt16()
    ulint24 = ULInt24()
    ulint32 = ULInt32()
    ulint40 = ULInt40()
    ulint48 = ULInt48()
    ulint56 = ULInt56()
    ulint64 = ULInt64()

    # signed little endian
    slint8 = SLInt8()
    slint16 = SLInt16()
    slint24 = SLInt24()
    slint32 = SLInt32()
    slint40 = SLInt40()
    slint48 = SLInt48()
    slint56 = SLInt56()
    slint64 = SLInt64()

    # optional
    optional_one = ConditionalField(UBInt8(), lambda m: m.options.b1)
    optional_two = ConditionalField(UBInt8(), lambda m: m.options.b2)

    # sequences with variable lengths
    ubseql = LengthField(UBInt8())
    ubseq = UBInt8Sequence(ubseql)

    sbseql = LengthField(UBInt8())
    sbseq = SBInt8Sequence(sbseql)

    # sequences with fixed lengths
    ubseqf = UBInt8Sequence(5)
    sbseqf = SBInt8Sequence(5)

    # don't change anything... for test coverage
    ulint16_value = FieldProperty(ulint16)
    ulint16_byte_string = FieldProperty(ulint16,
                                        onget=lambda v: str(v),
                                        onset=lambda v: struct.unpack(">H", v)[0])

    message_type = DispatchField(UBInt8())
    submessage_length = LengthField(UBInt16())
    submessage = DispatchTarget(submessage_length, message_type, {
        0xEF: SuperChild
    })

    # checksum starts after beginning magic, ends before
    # the checksum
    crc = CRCField(UBInt16(), crc16_ccitt, 2, -3)
    eof = Magic(b'~')


class TestMagic(unittest.TestCase):
    def test_magic(self):
        magic_field = Magic('\xAA').create_instance(None)
        self.assertRaises(SuitcaseProgrammingError, magic_field.setval)


class TestSuperField(unittest.TestCase):
    def _create_supermessage(self):
        s = SuperMessage()

        s.options.b1 = False
        s.options.b2 = True
        s.options.remaining = 0x1A

        # packed fields
        s.ubint8 = 0xAA
        s.ubint16 = 0xAABB
        s.ubint24 = 0xAABBCC
        s.ubint32 = 0xAABBCCDD
        s.ubint40 = 0xAABBCCDDEE
        s.ubint48 = 0xAABBCCDDEEFF
        s.ubint56 = 0xAABBCCDDEEFF00
        s.ubint64 = 0xAABBCCDDEEFF0011

        s.sbint8 = -100
        s.sbint16 = -1000
        s.sbint24 = -100000
        s.sbint32 = -100000000
        s.sbint40 = -10000000000
        s.sbint48 = -10000000000000
        s.sbint56 = -1000000000000000
        s.sbint64 = -100000000000000000

        s.ulint8 = 0xAA
        s.ulint16 = 0xAABB
        s.ulint16_byte_string = b'\xAA\xBB'
        s.ulint16_value = 0xBBAA

        s.ulint24 = 0xAABBCC
        s.ulint32 = 0xAABBCCDD
        s.ulint40 = 0xAABBCCDDEE
        s.ulint48 = 0xAABBCCDDEEFF
        s.ulint56 = 0xAABBCCDDEEFF00
        s.ulint64 = 0xAABBCCDDEEFF0011

        s.slint8 = -100
        s.slint16 = -1000
        s.slint24 = -100000
        s.slint32 = -100000000
        s.slint40 = -10000000000
        s.slint48 = -10000000000000
        s.slint56 = -1000000000000000
        s.slint64 = -100000000000000000

        s.optional_one = 1
        s.optional_two = 2

        s.ubseq = tuple(range(10))
        s.sbseq = tuple([x - 5 for x in range(10)])
        s.ubseqf = tuple(range(5))
        s.sbseqf = tuple([x - 2 for x in range(5)])

        sub = SuperChild()
        sub.remaining = b"Hello, this is SuperChild!"
        s.submessage = sub

        return s

    def test_super_message(self):
        sm = self._create_supermessage()
        packed = sm.pack()
        sm2 = SuperMessage.from_data(packed)

        for key, field in sm2:
            sm2_value = field.getval()
            if key == 'options':
                self.assertEqual(sm2_value.b1, sm.options.b1)
                self.assertEqual(sm2_value.b2, sm.options.b2)
                self.assertEqual(sm2_value.rest, sm.options.rest)

            elif key == 'submessage':
                self.assertEqual(sm2_value.ubseq, sm.ubseq)
                self.assertEqual(sm2_value.remaining, sm.submessage.remaining)
            elif key == 'optional_one':
                self.assertEqual(sm2_value, None)
            elif key == 'crc':
                pass  # validity check baked into protocol
            else:
                sm1_value = getattr(sm, key)
                self.assertEqual(sm2_value, sm1_value,
                                 "%s: %s != %s, types(%s, %s)" % (
                                     key, sm2_value, sm1_value, type(sm2_value), type(sm1_value)))

    def test_struct_packing_exceptions(self):
        # Verify that exceptions available all work
        sm = self._create_supermessage()
        sm.ubint8 = 0xFFFF  # too big
        self.assertRaises(SuitcasePackStructException, sm.pack)

    def test_other_pack_exception(self):
        sm = self._create_supermessage()
        sm._key_to_field['ubint8'].pack = raise_value_error
        self.assertRaises(SuitcasePackException, sm.pack)

    def test_repr_works(self):
        # just make sure nothing crashes
        sm = self._create_supermessage()
        repr(sm)


class TestFieldProperty(unittest.TestCase):
    def test_basic_setget(self):
        # define the message
        class MyMessage(Structure):
            _version = UBInt8Sequence(2)
            version = FieldProperty(_version,
                                    onget=lambda v: "%d.%02d" % (v[0], v[1]),
                                    onset=lambda v: tuple(int(six.b(x)) for x in v.split(".", 1)))

        msg = MyMessage.from_data(b'\x10\x03')
        self.assertEqual(msg._version, (16, 3))
        self.assertEqual(msg.version, "16.03")

        msg.version = "22.7"
        self.assertEqual(msg._version, (22, 7))
        self.assertEqual(msg.version, "22.07")


class BasicMessage(Structure):
    b1 = UBInt8()
    b2 = UBInt8()


class TestInstancePrototyping(unittest.TestCase):
    def test_independence(self):
        msg1 = BasicMessage()
        msg1.b1 = 10
        msg1.b2 = 20

        msg2 = BasicMessage()
        msg2.b1 = 20
        msg2.b2 = 30

        self.assertNotEqual(msg2.b1, msg1.b1)
        self.assertNotEqual(msg2.b2, msg1.b2)


class TestLengthField(unittest.TestCase):
    class MyMuiltipliedLengthMessage(Structure):
        length = LengthField(UBInt8(), multiplier=8)
        payload = VariableRawPayload(length)

    class MyLengthyMessage(Structure):
        length = LengthField(UBInt16())
        payload = VariableRawPayload(length)

    def test_basic_length_pack(self):
        msg = self.MyLengthyMessage()
        payload = b"Hello, world!"
        msg.payload = payload
        self.assertEqual(msg.pack(), b"\x00\x0DHello, world!")

    def test_basic_length_unpack(self):
        msg = self.MyLengthyMessage()
        msg.unpack(b'\x00\x0DHello, world!')
        self.assertEqual(msg.length, 13)
        self.assertEqual(msg.payload, b"Hello, world!")

    def test_multiplied_length_pack(self):
        msg = self.MyMuiltipliedLengthMessage()
        payload = b''.join(six.b(chr(x)) for x in range(8 * 4))
        msg.payload = payload
        self.assertEqual(msg.pack(), b'\x04' + payload)

    def test_bad_modulus_multiplier(self):
        cls = self.MyMuiltipliedLengthMessage
        msg = self.MyMuiltipliedLengthMessage()
        payload = b'\x01'  # 1-byte is not modulo 8
        msg.payload = payload
        self.assertRaises(SuitcaseProgrammingError, msg.pack)

    def test_multiplied_length_unpack(self):
        msg = self.MyMuiltipliedLengthMessage()
        msg.unpack(b'\x04' + b''.join([six.b(chr(x)) for x in range(8 * 4)]))
        self.assertEqual(msg.length, 4)
        self.assertEqual(msg.payload,
                         b''.join([six.b(chr(x)) for x in range(8 * 4)]))

    def test_unpack_insufficient_bytes(self):
        msg = self.MyLengthyMessage()
        self.assertRaises(SuitcaseParseError, msg.unpack, b'\x00\x0DHello')


class TestByteSequence(unittest.TestCase):
    def test_fixed_sequence(self):
        class MySeqMessage(Structure):
            type = UBInt8()
            byte_values = UBInt8Sequence(16)

        msg = MySeqMessage()
        msg.type = 0
        msg.byte_values = (0, 1, 2, 3, 4, 5, 6, 7, 8,
                           9, 10, 11, 12, 13, 14, 15)
        self.assertEqual(msg.pack(),
                         b'\x00\x00\x01\x02\x03\x04\x05\x06\x07\x08'
                         b'\t\n\x0b\x0c\r\x0e\x0f')

    def test_variable_sequence(self):
        class MyVarSeqMessage(Structure):
            type = UBInt8()
            length = LengthField(UBInt8())
            seq = UBInt8Sequence(length)

        msg = MyVarSeqMessage()
        msg.type = 0
        msg.seq = (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)

        self.assertEqual(msg.pack(),
                         b'\x00\n\x01\x02\x03\x04\x05\x06\x07\x08\t\n')
        msg2 = MyVarSeqMessage()
        msg2.unpack(msg.pack())
        self.assertEqual(msg2.length, 10)
        self.assertEqual(msg2.seq, (1, 2, 3, 4, 5, 6, 7, 8, 9, 10))

    def test_variable_sequence_nolength(self):
        class MyVarSeqMessage(Structure):
            s = VariableRawPayload(None)
            b = UBInt8Sequence(None)

        m = MyVarSeqMessage()
        m.s = b"Hello, world - "
        m.b = [0, 1, 2, 3, 4, 5]
        self.assertEqual(m.pack(),
                         b'Hello, world - \x00\x01\x02\x03\x04\x05')


class MyTargetMessage(Structure):
    # inherited from the parent message
    _length = LengthField(DependentField('length'))
    payload = VariableRawPayload(_length)


class MyOtherTargetMessage(Structure):
    #: Inherited from the parent message
    _length = LengthField(DependentField('length'))
    sequence = UBInt8Sequence(_length)


class MySimpleFixedPayload(Structure):
    number = UBInt32()


class MyPayloadMessage(Structure):
    payload = Payload()


class MyDefaultTargetMessage(Structure):
    _length = LengthField(DependentField('length'))
    sequence = UBInt8Sequence(_length)


class MyBasicDispatchMessage(Structure):
    type = DispatchField(UBInt8())
    length = LengthField(UBInt16())
    body = DispatchTarget(length, type, {
        0x00: MyTargetMessage,
        0x01: MyOtherTargetMessage,
        None: MyDefaultTargetMessage
    })


class MyDynamicLengthDispatchMessage(Structure):
    type = DispatchField(UBInt8())
    body = DispatchTarget(None, type, {
        0x1F: MySimpleFixedPayload,
        0xCC: MyPayloadMessage,
    })
    eof = Magic(b'EOF')


class BasicGreedy(Structure):
    a = UBInt8()
    b = UBInt8()
    payload = Payload()


class BoxedGreedy(Structure):
    sof = Magic(b'\xAA')
    a = LengthField(UBInt8())
    b = UBInt8()
    payload = Payload()
    c = UBInt16()
    d = VariableRawPayload(a)
    eof = Magic(b'\xbb')


class CRCGreedyTail(Structure):
    payload = Payload()
    magic = Magic(b'~~')
    crc = CRCField(UBInt32(), crc32, 0, -1)  # all (checksum zeroed)


class TestGreedyFields(unittest.TestCase):
    def test_unpack_basic_greedy(self):
        # Test case with trailing greedy payload
        m = BasicGreedy()
        m.unpack(b"\x00\x01Hello, you greedy, greedy World!")
        self.assertEqual(m.a, 0x00)
        self.assertEqual(m.b, 0x01)
        self.assertEqual(m.payload, b"Hello, you greedy, greedy World!")

    def test_unpack_boxed_greedy(self):
        # Test case where fields exist on either side of payload
        m = BoxedGreedy()
        m.unpack(b"\xaa\x05\x00This is the payload\x00\x15ABCDE\xbb")
        self.assertEqual(m.a, 5)
        self.assertEqual(m.b, 0)
        self.assertEqual(m.payload, b"This is the payload")
        self.assertEqual(m.c, 0x15)
        self.assertEqual(m.d, b"ABCDE")

    def test_unpack_boxed_greedy_error_before_greedy(self):
        # should start with \xAA but does not
        m = BoxedGreedy()
        self.assertRaises(SuitcaseParseError, m.unpack,
                          b"\x11\x05\x00This is the payload\x00\x15ABCDE\xbb")
        m._key_to_field['sof'].unpack = raise_value_error  # simulate fault
        self.assertRaises(SuitcaseParseError, m.unpack,
                          b"\xaa\x05\x00This is the payload\x00\x15ABCDE\xbb")

    def test_unpack_boxed_greedy_error_after_greedy(self):
        # should end in \xBB but does not
        m = BoxedGreedy()
        self.assertRaises(SuitcaseParseError, m.unpack,
                          b"\xaa\x05\x00This is the payload\x00\x15ABCDE\xAF")

        m._key_to_field['eof'].unpack = raise_value_error  # simlulate fault
        self.assertRaises(SuitcaseParseError, m.unpack,
                          b"\xaa\x05\x00This is the payload\x00\x15ABCDE\xbb")

    def test_pack_basic_greedy(self):
        m = BasicGreedy()
        m.a = 10
        m.b = 20
        m.payload = b"This is a packed greedy payload"
        self.assertEqual(m.pack(), b'\n\x14This is a packed greedy payload')

    def test_pack_boxed_greedy(self):
        m = BoxedGreedy()
        m.b = 20
        m.c = 300
        m.d = b"ABCD"
        m.payload = b"My length isn't declared and I am in the middle"
        self.assertEqual(m.pack(),
                         b"\xaa\x04\x14My length isn't declared and I am in the "
                         b"middle\x01,ABCD\xbb")

    def test_greedy_tail_pack_basic(self):
        m = CRCGreedyTail()
        m.payload = b"A GREEDY PAYLOAD FROM THE START"
        self.assertEqual(m.pack(), b'A GREEDY PAYLOAD FROM THE START~~\xfc\xce`=')

    def test_greedy_tail_unpack_basic(self):
        m = CRCGreedyTail.from_data(b'A GREEDY PAYLOAD FROM THE START~~\xfc\xce`=')
        self.assertEqual(m.payload, b"A GREEDY PAYLOAD FROM THE START")

    def test_greedy_tail_not_enough_bytes(self):
        self.assertRaises(SuitcaseParseError, CRCGreedyTail.from_data, b'~\xfc\xce`=')


class TestMessageDispatching(unittest.TestCase):
    def test_dispatch_packing(self):
        msg = MyBasicDispatchMessage()
        target_msg = MyTargetMessage()
        target_msg.payload = b"Hello, world!"
        msg.body = target_msg
        self.assertEqual(msg.pack(), b"\x00\x00\rHello, world!")

        msg2 = MyBasicDispatchMessage()
        target_msg = MyOtherTargetMessage()
        target_msg.sequence = (0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
        msg2.body = target_msg
        self.assertEqual(msg2.pack(),
                         b'\x01\x00\x0b\x00\x01\x02\x03\x04\x05\x06\x07\x08\t\n')

    def test_dispatch_unpacking(self):
        msg = MyBasicDispatchMessage()
        msg.unpack(b"\x00\x00\rHello, world!")
        self.assertEqual(msg.type, 0x00)
        body = msg.body
        self.assertEqual(body.payload, b"Hello, world!")

    def test_foreign_type(self):
        msg = MyBasicDispatchMessage()
        self.assertRaises(SuitcaseProgrammingError,
                          setattr, msg, 'body', SuperMessage())

    def test_default_dispatch(self):
        msg = MyBasicDispatchMessage()
        msg.unpack(b"\x10\x00\rHello, world!")
        self.assert_(isinstance(msg.body, MyDefaultTargetMessage))


class TestMessageDispatchingVariableLength(unittest.TestCase):
    def test_fixed_field_dispatch_packing(self):
        msg = MyDynamicLengthDispatchMessage()
        target_msg = MySimpleFixedPayload()
        target_msg.number = 0xFFEEFFEE
        msg.body = target_msg
        self.assertEqual(msg.pack(), b"\x1f\xff\xee\xff\xeeEOF")

    def test_variable_field_dispatch_packing(self):
        msg = MyDynamicLengthDispatchMessage()
        target_msg = MyPayloadMessage()
        target_msg.payload = b"~~~I'M SO DYNAMIC~~~"
        msg.body = target_msg
        self.assertEqual(msg.pack(), b"\xcc~~~I'M SO DYNAMIC~~~EOF")

    def test_dispatch_unpacking_fixed(self):
        msg = MyDynamicLengthDispatchMessage.from_data(b"\x1f\xff\xee\xff\xeeEOF")
        self.assertEqual(msg.body.number, 0xFFEEFFEE)

    def test_dispatch_unpacking_variable(self):
        msg = MyDynamicLengthDispatchMessage.from_data(b"\xcc~~~I'M SO DYNAMIC~~~EOF")
        self.assertEqual(msg.body.payload, b"~~~I'M SO DYNAMIC~~~")

    def test_dispatch_unpacking_fixed_bad_length(self):
        # too many bytes before EOF
        self.assertRaises(SuitcaseParseError, MyDynamicLengthDispatchMessage.from_data,
                          b"\x1f\xff\xee\xff\xeeEXTRA_EOF")


class TestBitFields(unittest.TestCase):
    def test_packing(self):
        field_proto = BitField(16,
                               nib1=BitNum(4),
                               nib2=BitNum(4),
                               nib3=BitNum(4),
                               nib4=BitNum(4),
                               )
        field = field_proto.create_instance(None)

        field.nib1 = 1
        field.nib2 = 2
        field.nib3 = 3
        field.nib4 = 4

        field2 = field_proto.create_instance(None)
        sio = six.BytesIO()
        field.pack(sio)
        field2.unpack(sio.getvalue())
        self.assertEqual(field2.nib1, 1)
        self.assertEqual(field2.nib2, 2)
        self.assertEqual(field2.nib3, 3)
        self.assertEqual(field2.nib4, 4)

    def test_bad_operations(self):
        field_proto = BitField(7,
                               num=BitNum(7))
        self.assertRaises(SuitcaseProgrammingError,
                          field_proto.create_instance, None)

    def test_explicit_field_override(self):
        field_proto = BitField(16, ULInt16(),
                               b1=BitBool(),
                               b2=BitBool(),
                               remaining=BitNum(14))
        inst = field_proto.create_instance(None)
        inst.b1 = True
        inst.b2 = False
        inst.remaining = 0x1EF
        sio = six.BytesIO()
        inst.pack(sio)

        # should be packed in little endian form
        self.assertEqual(sio.getvalue(), b"\xef\x81")

        inst2 = field_proto.create_instance(None)
        inst2.unpack(b"\xef\x81")
        self.assertEqual(inst.b1, inst2.b1)
        self.assertEqual(inst.b2, inst2.b2)
        self.assertEqual(inst.remaining, inst2.remaining)


# message where f2 is only defined if f2 is 255
class Conditional(Structure):
    f1 = UBInt8()
    f2 = ConditionalField(UBInt8(), lambda m: m.f1 == 255)


# message where f2 and f3 are only defined if f2 is 255,
# and f2 specifies a length for f3
class ConditionalLength(Structure):
    f1 = UBInt8()
    f2 = ConditionalField(LengthField(UBInt8()), lambda m: m.f1 == 255)
    f3 = ConditionalField(Payload(length_provider=f2), lambda m: m.f1 == 255)


# message body that is empty
class EmptyStructure(Structure):
    pass


# message where f3 is only defined if f2 is 255,
# and f3 is a dispatch target based on f1
class ConditionalDispatch(Structure):
    f1 = DispatchField(UBInt8())
    f2 = UBInt8()  # separate from f1
    length = LengthField(UBInt8())
    f3 = ConditionalField(
            DispatchTarget(length, f1, {0: EmptyStructure, 1: BasicMessage}),
            condition=lambda m: m.f2 == 255)


class ConditionalSubstructures(Structure):
    f1 = UBInt8()
    f2 = ConditionalField(SubstructureField(BasicMessage),
                          condition=lambda m: m.f1 < 10)
    f3 = ConditionalField(SubstructureField(BasicMessage),
                          condition=lambda m: m.f1 < 20)


class TestConditionalField(unittest.TestCase):
    def test_conditional_pack(self):
        m1 = Conditional()
        m1.f1 = 0x91
        self.assertEqual(m1.pack(), b'\x91')

        m2 = Conditional()
        m2.f1 = 0xFF
        m2.f2 = 0x09
        self.assertEqual(m2.pack(), b'\xff\x09')

    def test_conditional_rx(self):
        m1 = Conditional()
        m1.unpack(b'\x1f')
        self.assertEqual(m1.f1, 0x1f)
        self.assertEqual(m1.f2, None)

        m2 = Conditional()
        m2.unpack(b'\xff\x1f')
        self.assertEqual(m2.f1, 0xff)
        self.assertEqual(m2.f2, 0x1f)

    def test_conditional_length_pack(self):
        m1 = ConditionalLength()
        m1.f1 = 0x91
        self.assertEqual(m1.pack(), b'\x91')

        m2 = ConditionalLength()
        m2.f1 = 0xFF
        m2.f3 = b'\x01\x02\x03\x04\x05'
        self.assertEqual(m2.pack(), b'\xff\x05\x01\x02\x03\x04\x05')

    def test_conditional_length_unpack(self):
        m1 = ConditionalLength()
        m1.unpack(b'\x91')
        self.assertEqual(m1.f1, 0x91)
        self.assertEqual(m1.f2, None)
        self.assertEqual(m1.f3, None)

        m2 = ConditionalLength()
        m2.unpack(b'\xff\x05\x01\x02\x03\x04\x05')
        self.assertEqual(m2.f1, 0xff)
        self.assertEqual(m2.f2, 0x05)
        self.assertEqual(m2.f3, b'\x01\x02\x03\x04\x05')

    def test_conditional_dispatch(self):
        # Dispatch to an empty structure
        m1 = ConditionalDispatch()
        m1.unpack(b'\x00\xff\x00')
        self.assertEqual(m1.f1, 0)
        self.assertEqual(m1.f2, 255)
        self.assertEqual(m1.length, 0)
        self.assertIsInstance(m1.f3, EmptyStructure)

        # Dispatch to a non-empty structure
        m2 = ConditionalDispatch()
        m2.unpack(b'\x01\xff\x02\x11\x22')
        self.assertEqual(m2.f1, 1)
        self.assertEqual(m2.f2, 255)
        self.assertEqual(m2.length, 2)
        self.assertIsInstance(m2.f3, BasicMessage)
        self.assertEqual(m2.f3.b1, 0x11)
        self.assertEqual(m2.f3.b2, 0x22)

    def test_conditional_substructure_pack(self):
        m = ConditionalSubstructures()
        m.f1 = 9

        f2 = BasicMessage()
        f2.b1 = 0x55
        f2.b2 = 0x66
        m.f2 = f2

        f3 = BasicMessage()
        f3.b1 = 0x77
        f3.b2 = 0x88
        m.f3 = f3

        self.assertEqual(m.pack(), b"\x09\x55\x66\x77\x88")

        # Remove f2
        m.f1 = 10
        m.f2 = None
        self.assertEqual(m.pack(), b"\x0a\x77\x88")

        # Remove f3
        m.f1 = 20
        m.f3 = None
        self.assertEqual(m.pack(), b"\x14")

    def test_conditional_substructure_unpack(self):
        # Neither substructure is present
        m1 = ConditionalSubstructures.from_data(b"\x20")
        self.assertEqual(m1.f1, 0x20)
        self.assertEqual(m1.f2, None)
        self.assertEqual(m1.f3, None)

        # Include f3 but not f2
        m2 = ConditionalSubstructures()
        m2.unpack(b"\x10" +
                  b"\x11\x22")
        self.assertEqual(m2.f1, 0x10)
        self.assertEqual(m2.f2, None)
        self.assertIsInstance(m2.f3, BasicMessage)
        self.assertEqual(m2.f3.b1, 0x11)
        self.assertEqual(m2.f3.b2, 0x22)

        # Include both substructures
        m3 = ConditionalSubstructures()
        m3.unpack(b"\x00" +
                  b"\x11\x22" +
                  b"\x33\x44")
        self.assertEqual(m3.f1, 0x00)
        self.assertIsInstance(m3.f2, BasicMessage)
        self.assertEqual(m3.f2.b1, 0x11)
        self.assertEqual(m3.f2.b2, 0x22)
        self.assertIsInstance(m3.f3, BasicMessage)
        self.assertEqual(m3.f3.b1, 0x33)
        self.assertEqual(m3.f3.b2, 0x44)


class TestStructure(unittest.TestCase):
    def test_unpack_fewer_bytes_than_required(self):
        self.assertRaises(SuitcaseParseError, MySimpleFixedPayload.from_data, b'123')

    def test_unpack_more_bytes_than_required(self):
        self.assertRaises(SuitcaseParseError, MySimpleFixedPayload.from_data, b'12345')


# Test FieldArray and ConditionalField interaction
class ConditionalArrayElement(Structure):
    options = BitField(8,
                       cond8_present=BitBool(),
                       cond16_present=BitBool(),
                       unused=BitNum(6))
    cond8 = ConditionalField(UBInt8(), lambda m: m.options.cond8_present)
    cond16 = ConditionalField(UBInt16(), lambda m: m.options.cond16_present)


class ConditionalArrayGreedy(Structure):
    array = FieldArray(ConditionalArrayElement)


class ConditionalArrayGreedyAfter(Structure):
    length = LengthField(UBInt8())
    array = FieldArray(ConditionalArrayElement, length)
    greedy = Payload()


class TestConditionalArray(unittest.TestCase):
    def test_unpack_greedy(self):
        m = ConditionalArrayGreedy()
        m.unpack(b"\x00" +
                 b"\x80\x12" +
                 b"\x40\x34\x56" +
                 b"\xc0\x12\x34\x56")

        self.assertEqual(len(m.array), 4)

        self.assertEqual(m.array[0].options.cond8_present, 0)
        self.assertEqual(m.array[0].options.cond16_present, 0)
        self.assertEqual(m.array[0].cond8, None)
        self.assertEqual(m.array[0].cond16, None)

        self.assertEqual(m.array[1].options.cond8_present, 1)
        self.assertEqual(m.array[1].options.cond16_present, 0)
        self.assertEqual(m.array[1].cond8, 0x12)
        self.assertEqual(m.array[1].cond16, None)

        self.assertEqual(m.array[2].options.cond8_present, 0)
        self.assertEqual(m.array[2].options.cond16_present, 1)
        self.assertEqual(m.array[2].cond8, None)
        self.assertEqual(m.array[2].cond16, 0x3456)

        self.assertEqual(m.array[3].options.cond8_present, 1)
        self.assertEqual(m.array[3].options.cond16_present, 1)
        self.assertEqual(m.array[3].cond8, 0x12)
        self.assertEqual(m.array[3].cond16, 0x3456)

    def test_pack_greedy(self):
        m = ConditionalArrayGreedy()

        c1 = ConditionalArrayElement()
        c1.options.cond8_present = False
        c1.options.cond16_present = False
        c1.cond8 = None
        c1.cond16 = None
        m.array.append(c1)

        c2 = ConditionalArrayElement()
        c2.options.cond8_present = True
        c2.options.cond16_present = False
        c2.cond8 = 0x12
        c2.cond16 = None
        m.array.append(c2)

        c3 = ConditionalArrayElement()
        c3.options.cond8_present = False
        c3.options.cond16_present = True
        c3.cond8 = None
        c3.cond16 = 0x3456
        m.array.append(c3)

        c4 = ConditionalArrayElement()
        c4.options.cond8_present = True
        c4.options.cond16_present = True
        c4.cond8 = 0x12
        c4.cond16 = 0x3456
        m.array.append(c4)

        self.assertEqual(m.pack(),
                         b"\x00" +
                         b"\x80\x12" +
                         b"\x40\x34\x56" +
                         b"\xc0\x12\x34\x56")

    def test_unpack_greedy_after(self):
        m = ConditionalArrayGreedyAfter()
        m.unpack(b"\x05" +
                 b"\x00" +
                 b"\xc0\x12\x34\x56" +
                 b"Hello")

        self.assertEqual(len(m.array), 2)

        self.assertEqual(m.array[0].options.cond8_present, 0)
        self.assertEqual(m.array[0].options.cond16_present, 0)
        self.assertEqual(m.array[0].cond8, None)
        self.assertEqual(m.array[0].cond16, None)

        self.assertEqual(m.array[1].options.cond8_present, 1)
        self.assertEqual(m.array[1].options.cond16_present, 1)
        self.assertEqual(m.array[1].cond8, 0x12)
        self.assertEqual(m.array[1].cond16, 0x3456)

        self.assertEqual(m.greedy, b"Hello")

    def test_pack_greedy_after(self):
        m = ConditionalArrayGreedyAfter()

        c1 = ConditionalArrayElement()
        c1.options.cond8_present = False
        c1.options.cond16_present = False
        c1.cond8 = None
        c1.cond16 = None
        m.array.append(c1)

        c2 = ConditionalArrayElement()
        c2.options.cond8_present = True
        c2.options.cond16_present = True
        c2.cond8 = 0x12
        c2.cond16 = 0x3456
        m.array.append(c2)

        m.greedy = b"Hello"

        self.assertEqual(m.pack(),
                         b"\x05" +
                         b"\x00" +
                         b"\xc0\x12\x34\x56" +
                         b"Hello")


# Test TypeField and FieldArray interaction
class Structure8(Structure):
    value = UBInt8()


class Structure16(Structure):
    value = UBInt16()


class Structure32(Structure):
    value = UBInt32()


dispatch_mapping = {0x00: Structure8,
                    0x01: Structure16,
                    0x03: Structure32}

size_mapping = {0x00: 1,
                0x01: 2,
                None: 4}  # This handles the Structure32 case


class TypedArrayElement(Structure):
    type = TypeField(UBInt8(), size_mapping)
    value = DispatchTarget(type, type, dispatch_mapping)


class TypedArray(Structure):
    array = FieldArray(TypedArrayElement)


class TypeFieldNoLengthProvider(Structure):
    type = TypeField(UBInt8(), size_mapping)


class TypeFieldWrongSize(Structure):
    type = TypeField(UBInt8(), size_mapping)
    greedy = Payload(type)


class TestTypeField(unittest.TestCase):
    def test_unpack_typed_array(self):
        m = TypedArray.from_data(b"\x00\x12" +
                                 b"\x01\x12\x34" +
                                 b"\x03\x12\x34\x56\x78")

        self.assertEqual(m.array[0].type, 0x00)
        self.assertEqual(m.array[0].value.value, 0x12)

        self.assertEqual(m.array[1].type, 0x01)
        self.assertEqual(m.array[1].value.value, 0x1234)

        self.assertEqual(m.array[2].type, 0x03)
        self.assertEqual(m.array[2].value.value, 0x12345678)

    def test_pack_typed_array(self):
        m = TypedArray()

        e1 = TypedArrayElement()
        e1.value = Structure8.from_data(b"\x12")
        m.array.append(e1)

        e2 = TypedArrayElement()
        e2.value = Structure16.from_data(b"\x12\x34")
        m.array.append(e2)

        e3 = TypedArrayElement()
        e3.value = Structure32.from_data(b"\x12\x34\x56\x78")
        m.array.append(e3)

        self.assertEquals(m.pack(), b"\x00\x12" +
                          b"\x01\x12\x34" +
                          b"\x03\x12\x34\x56\x78")

    def test_repr(self):
        # Make sure nothing crashes
        m = TypedArray.from_data(b"\x03\x12\x34\x56\x78")
        repr(m)

    def test_no_length_provider(self):
        m = TypeFieldNoLengthProvider()
        m.type = 0x01
        self.assertRaises(SuitcaseException, m.pack)

    def test_wrong_size(self):
        m = TypeFieldWrongSize()
        m.type = 0x01  # Indicates a length of 2
        m.greedy = b"Hello"
        self.assertRaises(SuitcaseProgrammingError, m.pack)


# Test empty FieldArray
class BasicMessageArray(Structure):
    count = LengthField(UBInt8(), multiplier=2)
    array = FieldArray(BasicMessage, count)


# Test empty FieldArray with content after the array
class BasicMessageArrayAfter(Structure):
    count = LengthField(UBInt8(), multiplier=2)
    array = FieldArray(BasicMessage, count)
    after = UBInt8()


# Test a FieldArray that takes a number of elements instead of a size in bytes.
class BasicMessageArrayNumElements(Structure):
    count = LengthField(UBInt8())
    array = FieldArray(BasicMessage, num_elements_provider=count)


class TestFieldArray(unittest.TestCase):
    def test_pack_valid(self):
        m = BasicMessageArray()

        first = BasicMessage()
        first.b1 = 0x11
        first.b2 = 0x22
        second = BasicMessage()
        second.b1 = 0x33
        second.b2 = 0x44

        m.array = [first, second]

        self.assertEqual(m.pack(), b"\x02\x11\x22\x33\x44")

    def test_unpack_valid(self):
        m = BasicMessageArray.from_data(b"\x01AB")
        self.assertEqual(m.count, 1)
        self.assertEqual(len(m.array), 1)
        self.assertIsInstance(m.array[0], BasicMessage)
        self.assertEqual(m.array[0].b1, ord('A'))
        self.assertEqual(m.array[0].b2, ord('B'))

    def test_pack_empty(self):
        m = BasicMessageArray()

        m.array = []

        self.assertEqual(m.pack(), b"\x00")

    def test_unpack_empty(self):
        m = BasicMessageArray.from_data(b"\x00")
        self.assertEqual(m.count, 0)
        self.assertEqual(len(m.array), 0)

    def test_pack_after_valid(self):
        m = BasicMessageArrayAfter()

        m.array = []
        m.after = 1

        self.assertEqual(m.pack(), b"\x00\x01")

        # Populate the array.
        m2 = BasicMessageArrayAfter()
        first = BasicMessage()
        first.b1 = 0x22
        first.b2 = 0x33
        m2.array = [first]
        m2.after = 0

        self.assertEquals(m2.pack(), b"\x01\x22\x33\x00")

    def test_unpack_after_valid(self):
        m = BasicMessageArrayAfter.from_data(b"\x00\x11")

        self.assertEqual(m.count, 0)
        self.assertEqual(len(m.array), 0)
        self.assertEqual(m.after, 0x11)

        m2 = BasicMessageArrayAfter.from_data(b"\x01\x11\x22\x00")

        self.assertEqual(m2.count, 1)
        self.assertEqual(len(m2.array), 1)
        self.assertIsInstance(m2.array[0], BasicMessage)
        self.assertEqual(m2.array[0].b1, 0x11)
        self.assertEqual(m2.array[0].b2, 0x22)
        self.assertEqual(m2.after, 0)

    def test_pack_num_elements_valid(self):
        m = BasicMessageArrayNumElements()
        m.array = []
        self.assertEqual(m.pack(), b"\x00")

        messages = [BasicMessage(), BasicMessage(), BasicMessage()]
        for i, message in enumerate(messages):
            message.b1 = 0x10 + i
            message.b2 = 0x20 + i
            m.array.append(message)

        self.assertEqual(m.pack(), b"\x03\x10\x20\x11\x21\x12\x22")

    def test_unpack_num_elements_valid(self):
        m = BasicMessageArrayNumElements.from_data(b"\x02\x22\x33\x44\x55")

        self.assertEqual(m.count, 2)
        self.assertEqual(len(m.array), 2)
        self.assertIsInstance(m.array[0], BasicMessage)
        self.assertEqual(m.array[0].b1, 0x22)
        self.assertEqual(m.array[0].b2, 0x33)
        self.assertEqual(m.array[1].b1, 0x44)
        self.assertEqual(m.array[1].b2, 0x55)


# Test SubstructureField
class PascalString16(Structure):
    length = LengthField(UBInt16())
    value = Payload(length)


class NameStructure(Structure):
    first = SubstructureField(PascalString16)
    last = SubstructureField(PascalString16)


class NameStructureGreedyAfter(Structure):
    first = SubstructureField(PascalString16)
    last = SubstructureField(PascalString16)
    greedy = Payload()


class TestSubstructureField(unittest.TestCase):
    def test_pack_valid(self):
        m = NameStructure()
        m.first.value = b"John"
        m.last.value = b"Doe"
        self.assertEqual(m.pack(), b"\x00\x04John\x00\x03Doe")

    def test_unpack_valid(self):
        m = NameStructure().from_data(b"\x00\x04John\x00\x03Doe")
        self.assertIsInstance(m.first, PascalString16)
        self.assertEqual(m.first.value, b"John")
        self.assertIsInstance(m.last, PascalString16)
        self.assertEqual(m.last.value, b"Doe")

    def test_pack_greedy_after(self):
        m = NameStructureGreedyAfter()
        m.first.value = b"John"
        m.last.value = b"Doe"
        m.greedy = b"Hello World!"
        self.assertEqual(m.pack(), b"\x00\x04John\x00\x03DoeHello World!")

    def test_unpack_greedy_after(self):
        m = NameStructureGreedyAfter().from_data(b"\x00\x04John\x00\x03DoeHello World!")
        self.assertIsInstance(m.first, PascalString16)
        self.assertEqual(m.first.value, b"John")
        self.assertIsInstance(m.last, PascalString16)
        self.assertEqual(m.last.value, b"Doe")
        self.assertEqual(m.greedy, b"Hello World!")


class TestDiscoverableFields(unittest.TestCase):
    def test_dir(self):
        m = PascalString16()
        attrs = dir(m)
        self.assertTrue("length" in attrs)
        self.assertTrue("value" in attrs)


class TestKeywordArgumentInitialization(unittest.TestCase):
    def test_basic_assignment(self):
        m = PascalString16(value=b"Hello World!")
        self.assertEqual(m.pack(), b"\x00\x0cHello World!")

    def test_nested_assignment(self):
        m = NameStructure(first=PascalString16(value=b"John"),
                          last=PascalString16(value=b"Doe"))
        self.assertEqual(m.pack(), b"\x00\x04John\x00\x03Doe")

    def test_partial_assignment(self):
        m = NameStructure(first=PascalString16(value=b"John"))
        m.last = PascalString16(value=b"Doe")
        self.assertEqual(m.pack(), b"\x00\x04John\x00\x03Doe")


class StructureWithFieldAccessorAtTopLevel(Structure):
    bits = BitField(8, top=BitNum(3), bottom=BitNum(5))
    top = bits.top


class TestFieldAccessorAtTopLevel(unittest.TestCase):
    def test_access(self):
        s = StructureWithFieldAccessorAtTopLevel.from_data(b'\xA0')
        # Sanity check.
        self.assertEqual(s.bits.top, 0b101)
        self.assertEqual(s.bits.bottom, 0b00000)
        self.assertEqual(s.top, 0b101)

    def test_assignment(self):
        s = StructureWithFieldAccessorAtTopLevel.from_data(b'\x00')
        s.top = 0b111
        self.assertEqual(s.pack(), b'\xE0')


class Zero(Structure):
    body = Payload()


class One(Structure):
    first = UBInt8()
    second = UBInt16()


class FieldAccessorDispatch(Structure):
    bits = BitField(8, upper=BitNum(5), lower=BitNum(3))
    body = DispatchTarget(None, bits.upper, {
        0: Zero,
        1: One,
    })


class TestFieldAccessorDispatch(unittest.TestCase):
    def test_on_unpack(self):
        z = FieldAccessorDispatch.from_data(b'\x07Hello')
        self.assert_(isinstance(z.body, Zero))
        self.assertEqual(z.body.body, b'Hello')

        o = FieldAccessorDispatch.from_data(b'\x08\x11\x22\x33')
        self.assert_(isinstance(o.body, One))
        self.assertEqual(o.body.first, 0x11)
        self.assertEqual(o.body.second, 0x2233)

    def test_assignment(self):
        zero = FieldAccessorDispatch.from_data(b'\x00Hello there')
        self.assertEqual(zero.pack(), b'\x00Hello there')

        zero.body = One(first=1, second=2)
        self.assertEqual(zero.pack(), b'\x08\x01\x00\x02')


class FieldAccessorMultiple(Structure):
    # FieldAccessors can be referenced multiple times,
    # and more than one attribute of a given field may be referenced.
    # TODO: Figure out how to support LengthField(bits.upper)?
    bits = BitField(8, upper=BitNum(4), lower=BitNum(4))
    upper = bits.upper
    lower = bits.lower
    body = DispatchTarget(None, bits.lower, {
        0b1010: Structure16,  # 0x0A
        0b0101: One,  # 0x05
    })


class TestFieldAccessorMultiple(unittest.TestCase):
    def test_unpack(self):
        s = FieldAccessorMultiple.from_data(b'\x5A\xEE\x00')
        self.assert_(isinstance(s.body, Structure16))
        self.assertEqual(s.body.value, 0xEE00)

        s2 = FieldAccessorMultiple.from_data(
                b'\x35\xFF\x00\x01')
        self.assert_(isinstance(s2.body, One))
        self.assertEqual(s2.body.first, 0xFF)
        self.assertEqual(s2.body.second, 0x0001)

    def test_assignment(self):
        s = FieldAccessorMultiple(body=One(first=2, second=3))
        self.assertEqual(s.lower, 0b0101)
        self.assertEqual(s.body.first, 2)
        self.assertEqual(s.body.second, 3)
        self.assertEqual(s.pack(), b'\x05\x02\x00\x03')

        s.body = Structure16(value=0xFFFE)
        s.upper = 0b1100  # 0xC
        self.assertEqual(s.bits.lower, 0b1010)
        self.assertEqual(s.lower, s.bits.lower)
        self.assertEqual(s.upper, s.bits.upper)
        self.assertEqual(s.pack(), b'\xCA\xFF\xFE')


class _InnerDispatchType0(Structure):
    some_field = UBInt8()
    cond_sub = SubstructureField(ConditionalSubstructures)


class _InnerDispatchType1(Structure):
    one = UBInt8()
    two = UBInt8()


class _OuterDispatch(Structure):
    which = DispatchField(UBInt8())
    # NOTE: This setup will fail if greedy=False is not specified,
    # indicating that _OuterDispatch didn't consume the remainder of the data.
    # Because we don't _expect_ it to consume the rest of the data in the message,
    # we need to mark this as non-greedy.
    # TODO: We may be able to infer this from field ordering, or apply some heuristic
    # over the entire structure to correctly find the greedy field (payload at the end,
    # in this case).
    dispatch = DispatchTarget(None, which, {
        0x00: _InnerDispatchType0,
        0x01: _InnerDispatchType1,
    }, greedy=False)


class SubstructureDispatchFollowedByPayload(Structure):
    some_value = UBInt8()
    sub = SubstructureField(_OuterDispatch)
    another_value = UBInt8()
    payload = Payload()


class TestSubstructureDispatchFollowedByPayload(unittest.TestCase):
    def test_unpack(self):
        s = SubstructureDispatchFollowedByPayload.from_data(
            (
                # some_value
                b'\xF7'
                # sub: _OuterDispatch
                # - which
                b'\x00'
                # - _InnerDispatchType0
                # -- some_field
                b'\x7F'
                # -- cond_sub: ConditionalSubstructures
                # --- f1=19.
                b'\x13'
                # --- f3, 2 bytes.
                b'\x77\x88'
                # another_value
                b'\xE5'
                # payload
                b'This is my payload'
            )
        )

        self.assertEqual(s.some_value, 0xF7)
        self.assertEqual(s.sub.which, 0)
        self.assertEqual(s.sub.dispatch.some_field, 0x7F)
        self.assertEqual(s.sub.dispatch.cond_sub.f1, 19)
        self.assertEqual(s.sub.dispatch.cond_sub.f3.b1, 0x77)
        self.assertEqual(s.sub.dispatch.cond_sub.f3.b2, 0x88)
        self.assertEqual(s.another_value, 0xE5)
        self.assertEqual(s.payload, b'This is my payload')

    def test_pack(self):
        sub_dispatch = _InnerDispatchType0(
            some_field=0x01,
            cond_sub=ConditionalSubstructures(
                f1=11,
                f3=BasicMessage(
                    b1=0x01,
                    b2=0x02,
                )
            )
        )
        structure = SubstructureDispatchFollowedByPayload()
        structure.some_value = 0xFE
        structure.another_value = 0xE5
        structure.payload = b"Hello World!"
        structure.sub.dispatch = sub_dispatch

        self.assertEqual(structure.pack(),
                         b"\xFE\x00\x01\x0B\x01\x02\xE5Hello World!")


class FieldArrayWithDependentField(Structure):
    length = UBInt8()
    # Each entry in the array has a payload of length `length`
    entries = FieldArray(MyTargetMessage)


class SubstructureWithDependentField(Structure):
    length = UBInt16()
    sub = SubstructureField(MyTargetMessage)


class TestFieldArrayWithDependentField(unittest.TestCase):
    def test_unpack(self):
        data = (
            b"\x05"  # each message is 5 bytes
            b"Hello"
            b"World"
            b"Digi!"
        )
        structure = FieldArrayWithDependentField.from_data(data)
        self.assertEqual(len(structure.entries), 3)
        self.assertEqual(5, structure.length)
        self.assertEqual(structure.entries[0].payload, b"Hello")
        self.assertEqual(structure.entries[1].payload, b"World")
        self.assertEqual(structure.entries[2].payload, b"Digi!")

    @unittest.expectedFailure
    def test_dependent_field_automatically_updates(self):
        structure = FieldArrayWithDependentField()
        structure.entries.append(MyTargetMessage(payload=b"ABC"))
        self.assertEqual(3, structure.length)


class TestSubstructureWithDependentField(unittest.TestCase):
    def test_unpack(self):
        data = (
            b"\x00\x08"
            b"Hello..."
        )
        structure = SubstructureWithDependentField.from_data(data)
        self.assertEqual(structure.length, 8)
        self.assertEqual(structure.sub.payload, b"Hello...")

    @unittest.expectedFailure
    def test_auto_assignment_of_dependent_field(self):
        data = b"\x00\x01A"
        structure = SubstructureWithDependentField.from_data(data)
        structure.sub.payload = b"Hello, World!"
        # We would probably expect this to work! But it does not. :(
        self.assertEqual(structure.length, 13)


class MultipleGreedyFields(Structure):
    # TODO: Ideally we could do this particular example?
    payload1 = Payload()
    magic = Magic(b'\x00')
    payload2 = Payload()


class TestMultipleGreedyFields(unittest.TestCase):
    def test_unpack(self):
        with self.assertRaises(SuitcaseParseError) as caught:
            MultipleGreedyFields.from_data(
                b"Hello\0World"
            )
        self.assertIn(
            "found another greedy field", str(caught.exception),
            "Suitcase is not expected to support multiple greedy fields")

    def test_pack(self):
        # Even though we can't unpack such a structure, we can construct
        # and pack it.
        structure = MultipleGreedyFields(
            payload1=b"Hello,",
            payload2=b"World!",
        )
        self.assertEqual(structure.pack(), b"Hello,\x00World!")


class GreedyPayloadFollowedByNongreedyDispatch(Structure):
    payload = Payload()
    _nul = Magic(b'\x00')
    kind = DispatchField(UBInt8())
    rest = DispatchTarget(None, kind, {
        0x00: BasicMessage,
        0x01: MySimpleFixedPayload,
    }, greedy=False)


class TestGreedyPayloadFollowedByNongreedyDispatch(unittest.TestCase):
    def test_cannot_unpack(self):
        data = (
            b"Hello, World!"
            b"\x00"
            b"\x01"
            b"\xDE\xAD\xBE\xEF"
        )
        # Suitcase does not know how to handle a greedy field that is followed
        # by a nongreedy dispatch target. (If the length of the dispatch target
        # is indeterminate, the unpack process will feed the entire message in,
        # which obviously is not what we want.) The solution to this would be
        # to be able to direct Suitcase to parse the fields left-to-right, since
        # in this case the greedy payload is "boxed" by a null terminator.
        # But if that terminator was not present, this structure would be
        # impossible to parse anyway.
        self.assertRaises(SuitcaseParseError,
                          GreedyPayloadFollowedByNongreedyDispatch.from_data,
                          data)

    def test_pack(self):
        structure = GreedyPayloadFollowedByNongreedyDispatch()
        structure.payload = b"Hello!"
        structure.rest = MySimpleFixedPayload(number=0xDEADBEEF)
        self.assertEqual(structure.pack(), b"Hello!\x00\x01\xDE\xAD\xBE\xEF")


if __name__ == "__main__":
    unittest.main()
