import datetime
import io
from decimal import Decimal
from unittest import TestCase

from a38 import fields, models, validation
from a38.builder import Builder
from a38.diff import Diff

NS = "http://ivaservizi.agenziaentrate.gov.it/docs/xsd/fatture/v1.2"


class FieldTestMixin:
    field_class = fields.Field

    def get_field(self, *args, **kw):
        f = self.field_class(*args, **kw)
        f.set_name("sample")
        return f

    def test_xmltag(self):
        # XML tag is autogenerated from the field name
        f = self.get_field()
        self.assertEqual(f.get_xmltag(), "Sample")

        # But can be overridden with the xmltag argument
        f = self.get_field(xmltag="OtherName")
        self.assertEqual(f.get_xmltag(), "OtherName")

    def test_empty(self):
        f = self.get_field()

        # Validating a field with null=False raises an error
        self.assert_validates(f, None, result=None, errors=[
            "sample: missing value",
        ])

        # But null values are tolerated outside validation, while structures
        # are being filled
        self.assertIsNone(f.clean_value(None))

        # Values set to None are skipped in XML
        self.assertIsNone(self.to_xml(f, None))

    def test_nullable(self):
        f = self.get_field(null=True)
        self.assert_validates(f, None, result=None)
        self.assertIsNone(f.clean_value(None))

    def test_construct_default(self):
        f = self.get_field()
        self.assertIsNone(f.get_construct_default())

    def test_value(self):
        f = self.get_field()
        self.assert_validates(f, "value", result="value")

    def test_default(self):
        f = self.get_field(default="default")
        self.assertEqual(f.clean_value(None), "default")
        self.assertEqual(self.to_xml(f, None), "<T><Sample>default</Sample></T>")

    def test_xml(self):
        f = self.get_field(null=True)
        self.assertEqual(self.to_xml(f, "value"), "<T><Sample>value</Sample></T>")

    def test_to_python_none(self):
        f = self.get_field()
        self.assert_to_python_works(f, None)

    def test_diff_none(self):
        f = self.get_field()
        self.assert_diff_empty(f, None, None)

    def assert_validates(self, field, value, result, warnings=[], errors=[]):
        val = validation.Validation()
        validated = field.validate(val, value)
        self.assertEqual([str(x) for x in val.warnings], warnings)
        self.assertEqual([str(x) for x in val.errors], errors)
        self.assertEqual(validated, result)

    def assert_to_python_works(self, field, value, **kw):
        kw.setdefault("namespace", False)
        py = field.to_python(value, **kw)
        try:
            parsed = eval(py)
        except Exception as e:
            self.fail("cannot parse generated python {}: {}".format(repr(py), str(e)))
        clean = field.clean_value(parsed)
        self.assertEqual(clean, field.clean_value(value))

    def assert_diff_empty(self, field, first, second):
        """
        Check that the field diff between the two values is empty
        """
        res = Diff()
        field.diff(res, first, second)
        if res.differences:
            self.assertEqual([(d.prefix, d.field, d.first, d.second) for d in res.differences], [])

    def assert_diff(self, field, first, second, expected):
        """
        Check that the field diff between the two differing values, is as
        expected
        """
        res = Diff()
        field.diff(res, first, second)
        self.assertEqual([str(d) for d in res.differences], expected)

    def assert_field_diff(self, field, first, second):
        """
        Check that the field diff, from a non-composite field, between the two
        differing values, is as expected
        """
        self.assert_diff(field, first, None, ["sample: second is not set"])
        self.assert_diff(field, None, first, ["sample: first is not set"])
        self.assert_diff(field, second, None, ["sample: second is not set"])
        self.assert_diff(field, None, second, ["sample: first is not set"])
        self.assert_diff(field, first, second, ["sample: first: {}, second: {}".format(
            field.to_str(field.clean_value(first)),
            field.to_str(field.clean_value(second)))])

    def to_xml(self, field, value):
        """
        Serialize the field to XML. Returns None is the field generated no
        value in the XML.
        """
        builder = Builder()
        with builder.element("T"):
            field.to_xml(builder, value)
        tree = builder.get_tree()
        root = tree.getroot()
        if not list(root):
            return None
        with io.StringIO() as out:
            tree.write(out, encoding="unicode")
            return out.getvalue()

    def mkdt(self, ye, mo, da, ho, mi, se=0, tz=None):
        if tz is None:
            tz = fields.DateTimeField.tz_rome
        return tz.localize(datetime.datetime(ye, mo, da, ho, mi, se))


class TestField(FieldTestMixin, TestCase):
    pass


class TestStringField(FieldTestMixin, TestCase):
    field_class = fields.StringField

    def test_value(self):
        f = self.get_field()
        self.assert_validates(f, "value", result="value")
        self.assert_validates(f, 12, result="12")

    def test_default(self):
        f = self.get_field(default="default")
        self.assertEqual(f.clean_value(None), "default")
        self.assertEqual(self.to_xml(f, None), "<T><Sample>default</Sample></T>")

    def test_length(self):
        f = self.get_field(length=3)
        self.assert_validates(f, "va", result="va", errors=[
            "sample: 'va' should be at least 3 characters long",
        ])
        self.assert_validates(f, "valu", result="valu", errors=[
            "sample: 'valu' should be no more than 3 characters long",
        ])
        self.assert_validates(f, 1.15, result="1.15", errors=[
            "sample: '1.15' should be no more than 3 characters long",
        ])
        self.assert_validates(f, "val", result="val")
        self.assert_validates(f, 1.2, result="1.2")

    def test_min_length(self):
        f = self.get_field(min_length=3)
        self.assert_validates(f, "va", result="va", errors=[
            "sample: 'va' should be at least 3 characters long",
        ])
        self.assert_validates(f, "valu", result="valu")
        self.assert_validates(f, "val", result="val")
        self.assert_validates(f, 1.2, result="1.2")
        self.assert_validates(f, 1.15, result="1.15")

    def test_max_length(self):
        f = self.get_field(max_length=3)
        self.assert_validates(f, "v", result="v")
        self.assert_validates(f, "va", result="va")
        self.assert_validates(f, "val", result="val")
        self.assert_validates(f, "valu", result="valu", errors=[
            "sample: 'valu' should be no more than 3 characters long",
        ])

    def test_choices(self):
        f = self.get_field(choices=("A", "B"))
        self.assert_validates(f, "A", result="A")
        self.assert_validates(f, "B", result="B")
        self.assert_validates(f, "C", result="C", errors=[
            "sample: 'C' is not a valid choice for this field",
        ])
        self.assert_validates(f, "a", result="a", errors=[
            "sample: 'a' is not a valid choice for this field",
        ])
        self.assert_validates(f, None, result=None, errors=[
            "sample: missing value",
        ])

    def test_choices_nullable(self):
        f = self.get_field(choices=("A", "B"), null=True)
        self.assert_validates(f, "A", result="A")
        self.assert_validates(f, "B", result="B")
        self.assert_validates(f, None, result=None)
        self.assert_validates(f, "C", result="C", errors=[
            "sample: 'C' is not a valid choice for this field",
        ])
        self.assert_validates(f, "a", result="a", errors=[
            "sample: 'a' is not a valid choice for this field",
        ])

    def test_to_python(self):
        f = self.get_field()
        self.assert_to_python_works(f, "")
        self.assert_to_python_works(f, "foo")
        self.assert_to_python_works(f, "'\"\n")
        self.assert_to_python_works(f, r"\d\t\n")

    def test_diff(self):
        f = self.get_field()
        self.assert_diff_empty(f, "", "")
        self.assert_diff_empty(f, "a", "a")
        self.assert_field_diff(f, "a", "b")

    def test_xml(self):
        f = self.get_field(null=True)
        self.assertEqual(self.to_xml(f, "value"), "<T><Sample>value</Sample></T>")


class TestIntegerField(FieldTestMixin, TestCase):
    field_class = fields.IntegerField

    def test_value(self):
        f = self.get_field()
        self.assert_validates(f, 12, result=12)
        self.assert_validates(f, "12", result=12)
        self.assert_validates(f, 12.3, result=12)
        self.assert_validates(f, "foo", result="foo", errors=[
            "sample: invalid literal for int() with base 10: 'foo'",
        ])

    def test_default(self):
        f = self.get_field(default=7)
        self.assertEqual(f.clean_value(None), 7)
        self.assertEqual(self.to_xml(f, None), "<T><Sample>7</Sample></T>")

    def test_max_length(self):
        f = self.get_field(max_length=3)
        self.assert_validates(f, 1, result=1)
        self.assert_validates(f, 12, result=12)
        self.assert_validates(f, 123, result=123)
        self.assert_validates(f, 1234, result=1234, errors=[
            "sample: '1234' should be no more than 3 digits long",
        ])

    def test_choices(self):
        f = self.get_field(choices=(1, 2))
        self.assert_validates(f, 1, result=1)
        self.assert_validates(f, 2, result=2)
        self.assert_validates(f, 3, result=3, errors=[
            "sample: 3 is not a valid choice for this field",
        ])
        self.assert_validates(f, None, result=None, errors=[
            "sample: missing value",
        ])

    def test_choices_nullable(self):
        f = self.get_field(choices=(1, 2), null=True)
        self.assert_validates(f, 1, result=1)
        self.assert_validates(f, 2, result=2)
        self.assert_validates(f, 3, result=3, errors=[
            "sample: 3 is not a valid choice for this field",
        ])
        self.assert_validates(f, None, result=None)

    def test_to_python(self):
        f = self.get_field()
        self.assert_to_python_works(f, 1)
        self.assert_to_python_works(f, 123456)
        self.assert_to_python_works(f, 3 ** 80)

    def test_diff(self):
        f = self.get_field()
        self.assert_diff_empty(f, 1, "1")
        self.assert_field_diff(f, 1, "2")

    def test_xml(self):
        f = self.get_field(null=True)
        self.assertEqual(self.to_xml(f, 1), "<T><Sample>1</Sample></T>")


class TestDecimalField(FieldTestMixin, TestCase):
    field_class = fields.DecimalField

    def test_value(self):
        f = self.get_field()
        self.assert_validates(f, 12, result=Decimal("12.00"))
        self.assert_validates(f, "12", result=Decimal("12.00"))
        self.assert_validates(f, "12.345", result=Decimal("12.35"))
        self.assert_validates(f, "foo", result="foo", errors=[
            "sample: 'foo' cannot be converted to Decimal",
        ])

    def test_decimals_fixed(self):
        f = self.get_field(decimals=3)
        self.assert_validates(f, 12, result=Decimal("12.000"))
        self.assert_validates(f, "12", result=Decimal("12.000"))
        self.assert_validates(f, "12.345", result=Decimal("12.345"))
        self.assert_validates(f, "12.345678", result=Decimal("12.346"))
        self.assert_validates(f, "foo", result="foo", errors=[
            "sample: 'foo' cannot be converted to Decimal",
        ])

    def test_decimals_range(self):
        f = self.get_field(decimals=(2, 6))
        self.assert_validates(f, 12, result=Decimal("12.00"))
        self.assert_validates(f, "12", result=Decimal("12.00"))
        self.assert_validates(f, "12.345", result=Decimal("12.345"))
        self.assert_validates(f, "12.345678", result=Decimal("12.345678"))
        self.assert_validates(f, "12.3456789", result=Decimal("12.345679"))
        self.assert_validates(f, "foo", result="foo", errors=[
            "sample: 'foo' cannot be converted to Decimal",
        ])

    def test_default(self):
        f = self.get_field(default="7.0")
        self.assertEqual(f.clean_value(None), Decimal("7.0"))
        self.assertEqual(self.to_xml(f, None), "<T><Sample>7.00</Sample></T>")

    def test_max_length(self):
        f = self.get_field(max_length=4)
        self.assert_validates(f, 1, result=Decimal("1.00"))
        # 12 becomes 12.00 which is 5 characters long on a max_length of 4
        self.assert_validates(f, 12, result=Decimal("12.00"), errors=[
            "sample: '12.00' should be no more than 4 digits long",
        ])

    def test_choices(self):
        f = self.get_field(choices=("1.1", "2.2"), decimals=1)
        self.assert_validates(f, "1.1", result=Decimal("1.1"))
        self.assert_validates(f, Decimal("2.2"), result=Decimal("2.2"))
        # 1.1 does not have an exact decimal representation, but clean_value
        # will constrain it to the configured number of digits
        self.assert_validates(f, 1.1, result=Decimal("1.1"))
        self.assert_validates(f, None, result=None, errors=[
            "sample: missing value",
        ])

    def test_choices_nullable(self):
        f = self.get_field(choices=("1.1", "2.2"), null=True, decimals=1)
        self.assert_validates(f, "1.1", result=Decimal("1.1"))
        self.assert_validates(f, Decimal("2.2"), result=Decimal("2.2"))
        self.assert_validates(f, None, result=None)
        # 1.1 does not have an exact decimal representation, but gets fit into
        # the allowed decimals
        self.assert_validates(f, 1.1, result=Decimal("1.1"))

    def test_to_python(self):
        f = self.get_field()
        self.assert_to_python_works(f, "1.2")
        self.assert_to_python_works(f, Decimal("1.20"))

    def test_diff(self):
        f = self.get_field()
        self.assert_diff_empty(f, Decimal("1.0"), "1.0")
        self.assert_diff_empty(f, "1.0001", "1.0002")
        self.assert_field_diff(f, "1.1", "1.2")

    def test_xml(self):
        f = self.get_field(null=True)
        self.assertEqual(self.to_xml(f, "12.345"), "<T><Sample>12.35</Sample></T>")
        self.assertEqual(self.to_xml(f, "34.567"), "<T><Sample>34.57</Sample></T>")


class TestDateField(FieldTestMixin, TestCase):
    field_class = fields.DateField

    def test_value(self):
        f = self.get_field()
        self.assert_validates(f, datetime.date(2019, 1, 2), result=datetime.date(2019, 1, 2))
        self.assert_validates(f, "2019-01-02", result=datetime.date(2019, 1, 2))
        self.assert_validates(f, datetime.datetime(2019, 1, 2, 12, 30), result=datetime.date(2019, 1, 2))
        self.assert_validates(f, "foo", result="foo", errors=[
            "sample: Date 'foo' does not begin with YYYY-mm-dd",
        ])
        self.assert_validates(f, [123], result=[123], errors=[
            "sample: '[123]' is not an instance of str, datetime.date or datetime.datetime",
        ])

    def test_default(self):
        f = self.get_field(default="2019-01-02")
        self.assertEqual(f.clean_value(None), datetime.date(2019, 1, 2))
        self.assertEqual(self.to_xml(f, None), "<T><Sample>2019-01-02</Sample></T>")

    def test_choices(self):
        f = self.get_field(choices=("2019-01-01", "2019-01-02"))
        self.assert_validates(f, "2019-01-01", result=datetime.date(2019, 1, 1))
        self.assert_validates(f, "2019-01-02", result=datetime.date(2019, 1, 2))
        self.assert_validates(f, "2019-01-03", result=datetime.date(2019, 1, 3), errors=[
            "sample: datetime.date(2019, 1, 3) is not a valid choice for this field",
        ])
        self.assert_validates(f, None, result=None, errors=[
            "sample: missing value",
        ])

    def test_choices_nullable(self):
        f = self.get_field(choices=("2019-01-01", "2019-01-02"), null=True)
        self.assert_validates(f, "2019-01-01", result=datetime.date(2019, 1, 1))
        self.assert_validates(f, "2019-01-02", result=datetime.date(2019, 1, 2))
        self.assert_validates(f, "2019-01-03", result=datetime.date(2019, 1, 3), errors=[
            "sample: datetime.date(2019, 1, 3) is not a valid choice for this field",
        ])
        self.assert_validates(f, None, result=None)

    def test_to_python(self):
        f = self.get_field()
        self.assert_to_python_works(f, "2019-01-03")
        self.assert_to_python_works(f, datetime.date(2019, 2, 4))

    def test_diff(self):
        f = self.get_field()
        self.assert_diff_empty(f, datetime.date(2019, 1, 1), "2019-01-01")
        self.assert_field_diff(f, datetime.date(2019, 1, 1), "2019-01-02")
        self.assert_field_diff(f, "2019-01-01", "2019-01-02")

    def test_xml(self):
        f = self.get_field(null=True)
        self.assertEqual(self.to_xml(f, datetime.date(2019, 1, 2)), "<T><Sample>2019-01-02</Sample></T>")
        self.assertEqual(self.to_xml(f, "2019-01-02"), "<T><Sample>2019-01-02</Sample></T>")


class TestDateTimeField(FieldTestMixin, TestCase):
    field_class = fields.DateTimeField

    def test_value(self):
        f = self.get_field()
        self.assert_validates(f, datetime.datetime(2019, 1, 2, 12, 30), result=self.mkdt(2019, 1, 2, 12, 30))
        self.assert_validates(f, "2019-01-02T12:30:00", result=self.mkdt(2019, 1, 2, 12, 30))
        self.assert_validates(f, datetime.datetime(2019, 1, 2, 12, 30), result=self.mkdt(2019, 1, 2, 12, 30))
        self.assert_validates(f, "foo", result="foo", errors=[
            "sample: ISO string too short",
        ])
        self.assert_validates(f, [123], result=[123], errors=[
            "sample: '[123]' is not an instance of str, datetime.date or datetime.datetime",
        ])

    def test_default(self):
        f = self.get_field(default="2019-01-02T12:30:00")
        self.assertEqual(f.clean_value(None), self.mkdt(2019, 1, 2, 12, 30))
        self.assertEqual(self.to_xml(f, None), "<T><Sample>2019-01-02T12:30:00+01:00</Sample></T>")

    def test_choices(self):
        f = self.get_field(choices=("2019-01-01T12:00:00", "2019-01-02T12:30:00"))
        self.assert_validates(f, "2019-01-01T12:00:00", result=self.mkdt(2019, 1, 1, 12, 00))
        self.assert_validates(f, "2019-01-02T12:30:00", result=self.mkdt(2019, 1, 2, 12, 30))
        self.assert_validates(f, self.mkdt(2019, 1, 2, 12, 15), result=self.mkdt(2019, 1, 2, 12, 15), errors=[
            "sample: 2019-01-02T12:15:00+01:00 is not a valid choice for this field",
        ])
        self.assert_validates(f, None, result=None, errors=[
            "sample: missing value",
        ])

    def test_choices_nullable(self):
        f = self.get_field(choices=("2019-01-01T12:00:00", "2019-01-02T12:30:00"), null=True)
        self.assert_validates(f, "2019-01-01T12:00:00", result=self.mkdt(2019, 1, 1, 12, 00))
        self.assert_validates(f, "2019-01-02T12:30:00", result=self.mkdt(2019, 1, 2, 12, 30))
        self.assert_validates(f, None, result=None)
        self.assert_validates(f, self.mkdt(2019, 1, 2, 12, 15), result=self.mkdt(2019, 1, 2, 12, 15), errors=[
            "sample: 2019-01-02T12:15:00+01:00 is not a valid choice for this field",
        ])

    def test_to_python(self):
        f = self.get_field()
        self.assert_to_python_works(f, "2019-01-03T04:05:06")
        self.assert_to_python_works(f, self.mkdt(2019, 1, 2, 3, 4, 5))

    def test_diff(self):
        f = self.get_field()
        self.assert_diff_empty(f, self.mkdt(2019, 1, 2, 3, 4, 5), "2019-01-02T03:04:05+01:00")
        self.assert_field_diff(f, self.mkdt(2019, 1, 2, 3, 4, 5), self.mkdt(2019, 1, 2, 3, 4, 6))
        self.assert_field_diff(f, self.mkdt(2019, 1, 2, 3, 4, 5), "2019-01-02T03:04:05+02:00")

    def test_xml(self):
        f = self.get_field(null=True)
        self.assertEqual(
            self.to_xml(f, self.mkdt(2019, 1, 2, 12, 30)),
            "<T><Sample>2019-01-02T12:30:00+01:00</Sample></T>")
        self.assertEqual(self.to_xml(f, "2019-01-02T12:13:14"), "<T><Sample>2019-01-02T12:13:14+01:00</Sample></T>")


class TestProgressivoInvioField(FieldTestMixin, TestCase):
    field_class = fields.ProgressivoInvioField

    def test_construct_default(self):
        f = self.get_field()

        # The field generates always different, always increasing values
        a = f.get_construct_default()
        b = f.get_construct_default()
        c = f.get_construct_default()
        d = f.get_construct_default()
        self.assertNotEqual(a, b)
        self.assertNotEqual(a, c)
        self.assertNotEqual(a, d)
        self.assertNotEqual(b, c)
        self.assertNotEqual(b, d)
        self.assertNotEqual(c, d)
        self.assertLess(a, b)
        self.assertLess(b, c)
        self.assertLess(c, d)

    def test_to_python(self):
        f = self.get_field()
        self.assert_to_python_works(f, "BFABFAF")


class Sample(models.Model):
    name = fields.StringField()
    value = fields.IntegerField()


class TestModelField(FieldTestMixin, TestCase):
    field_class = fields.ModelField

    def get_field(self, *args, **kw):
        return super().get_field(Sample, *args, **kw)

    def test_construct_default(self):
        f = self.get_field()
        value = f.get_construct_default()
        self.assertIsInstance(value, Sample)
        self.assertIsNone(value.name)
        self.assertIsNone(value.value)

    def test_empty(self):
        super().test_empty()

        # Empty models are skipped in XML
        f = self.get_field()
        self.assertIsNone(self.to_xml(f, Sample()))

    def test_value(self):
        f = self.get_field()
        self.assert_validates(f, Sample("test", 7), result=Sample("test", 7))

    def test_default(self):
        f = self.get_field(default=Sample("test", 7))
        self.assertEqual(f.clean_value(None), Sample("test", 7))
        self.assertEqual(self.to_xml(f, None), "<T><Sample><Name>test</Name><Value>7</Value></Sample></T>")

    def test_to_python(self):
        f = self.get_field()
        self.assert_to_python_works(f, Sample("test", 7))

    def test_diff(self):
        f = self.get_field()
        self.assert_diff_empty(f, Sample("test", 7), Sample("test", "7"))
        self.assert_diff(f, Sample("test", 6), None, [
            "sample: second is not set",
        ])
        self.assert_diff(f, Sample("test", 6), Sample("test", 7), [
            "sample.value: first: 6, second: 7",
        ])
        self.assert_diff(f, Sample("test1", 6), Sample("test2", 7), [
            "sample.name: first: test1, second: test2",
            "sample.value: first: 6, second: 7",
        ])

    def test_xml(self):
        f = self.get_field(null=True)
        self.assertEqual(self.to_xml(f, Sample("test", 7)), "<T><Sample><Name>test</Name><Value>7</Value></Sample></T>")


class TestModelListField(FieldTestMixin, TestCase):
    field_class = fields.ModelListField

    def get_field(self, *args, **kw):
        return super().get_field(Sample, *args, **kw)

    def test_construct_default(self):
        f = self.get_field()
        value = f.get_construct_default()
        self.assertEqual(value, [])

    def test_value(self):
        f = self.get_field()
        self.assert_validates(f, [], result=[], errors=[
            "sample: missing value",
        ])
        self.assert_validates(f, [Sample("test", 7)], result=[Sample("test", 7)])

        f = self.get_field(null=True)
        self.assert_validates(f, [], result=[])

    def test_min_num(self):
        f = self.get_field(min_num=2)
        self.assertEqual(f.get_construct_default(), [Sample(), Sample()])
        self.assertEqual(f.clean_value([Sample(), Sample(), Sample()]), [Sample(), Sample()])

        self.assert_validates(f, [Sample("test", 7)], result=[Sample("test", 7)], errors=[
            "sample: list must have at least 2 elements, but has only 1",
        ])
        self.assert_validates(f, [Sample("test", 6), Sample("test", 7)], result=[Sample("test", 6), Sample("test", 7)])

    def test_default(self):
        f = self.get_field(default=[Sample("test", 7)])
        self.assertEqual(f.clean_value(None), [Sample("test", 7)])
        self.assertEqual(self.to_xml(f, None), "<T><Sample><Name>test</Name><Value>7</Value></Sample></T>")

    def test_to_python(self):
        f = self.get_field()
        self.assert_to_python_works(f, [Sample("test", 7)])

    def test_diff(self):
        f = self.get_field()
        self.assert_diff_empty(f, [], None)
        self.assert_diff_empty(f, [Sample("test", 7)], [Sample("test", "7")])
        self.assert_diff_empty(f, [Sample("test", 6)], [Sample("test", 6), None])
        self.assert_diff(f, [Sample("test", 6)], None, [
            "sample: second is not set",
        ])
        self.assert_diff(f, [Sample("test", 6)], [], [
            "sample: second is not set",
        ])
        self.assert_diff(f, [Sample("test", 6)], [Sample("test", 7)], [
            "sample.0.value: first: 6, second: 7",
        ])
        self.assert_diff(f, [Sample("test", 6)], [Sample("test", 6), Sample("test", 7)], [
            "sample: second has 1 extra element",
        ])
        self.assert_diff(f, [Sample("test", 6)], [Sample("test", 5), Sample("test", 7)], [
            "sample.0.value: first: 6, second: 5",
            "sample: second has 1 extra element",
        ])

    def test_xml(self):
        f = self.get_field(null=True)
        self.assertIsNone(self.to_xml(f, []))
        self.assertEqual(
            self.to_xml(f, [Sample("test", 7)]),
            "<T><Sample><Name>test</Name><Value>7</Value></Sample></T>")


class TestListField(FieldTestMixin, TestCase):
    field_class = fields.ListField

    def get_field(self, *args, **kw):
        return super().get_field(fields.StringField(), *args, **kw)

    def test_construct_default(self):
        f = self.get_field()
        value = f.get_construct_default()
        self.assertEqual(value, [])

    def test_value(self):
        f = self.get_field()
        self.assert_validates(f, [], result=[], errors=[
            "sample: missing value",
        ])
        self.assert_validates(f, ["test1", "test2"], result=["test1", "test2"])

        f = self.get_field(null=True)
        self.assert_validates(f, [], result=[])

    def test_min_num(self):
        f = self.get_field(min_num=2)
        self.assertEqual(f.get_construct_default(), [None, None])
        self.assertEqual(f.clean_value([None, None, None]), [None, None])

        self.assert_validates(f, ["test1"], result=["test1"], errors=[
            "sample: list must have at least 2 elements, but has only 1",
        ])
        self.assert_validates(f, ["test1", "test2"], result=["test1", "test2"])

    def test_default(self):
        f = self.get_field(default=["test1", "test2"])
        self.assertEqual(f.clean_value(None), ["test1", "test2"])
        self.assertEqual(self.to_xml(f, None), "<T><Sample>test1</Sample><Sample>test2</Sample></T>")

    def test_to_python(self):
        f = self.get_field()
        self.assert_to_python_works(f, ["test1", "foo"])

        f = super().get_field(fields.DateTimeField())
        self.assert_to_python_works(f, [self.mkdt(2019, 1, 2, 3, 4), self.mkdt(2019, 2, 3, 4, 5)])

    def test_diff(self):
        f = self.get_field()
        self.assert_diff_empty(f, [], None)
        self.assert_diff_empty(f, ["test"], ["test"])
        self.assert_diff_empty(f, ["test"], ["test", None])
        self.assert_diff(f, ["test"], ["test1"], [
            "sample.0: first: test, second: test1",
        ])
        self.assert_diff(f, ["test"], ["test", "test"], [
            "sample: second has 1 extra element",
        ])
        self.assert_diff(f, ["test"], ["test1", "test2"], [
            "sample.0: first: test, second: test1",
            "sample: second has 1 extra element",
        ])

    def test_xml(self):
        f = self.get_field(null=True)
        self.assertIsNone(self.to_xml(f, []))
        self.assertEqual(self.to_xml(f, ["test", "foo"]), "<T><Sample>test</Sample><Sample>foo</Sample></T>")
