#    Copyright 2013 Red Hat, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import datetime
from typing import Any
from unittest import mock
import warnings

import netaddr
import testtools

from oslo_versionedobjects import _utils
from oslo_versionedobjects import base as obj_base
from oslo_versionedobjects import exception
from oslo_versionedobjects import fields
from oslo_versionedobjects import test


class FakeFieldType(fields.FieldType[str]):
    def coerce(self, obj, attr, value):
        return f'*{value}*'

    def to_primitive(self, obj, attr, value):
        return f'!{value}!'

    def from_primitive(self, obj, attr, value):
        return value[1:-1]

    def get_schema(self):
        return {'type': ['foo']}


class FakeEnum(fields.Enum):
    FROG = "frog"
    PLATYPUS = "platypus"
    ALLIGATOR = "alligator"

    ALL = (FROG, PLATYPUS, ALLIGATOR)

    def __init__(self, **kwargs):
        super().__init__(valid_values=FakeEnum.ALL, **kwargs)


class FakeEnumAlt(fields.Enum):
    FROG = "frog"
    PLATYPUS = "platypus"
    AARDVARK = "aardvark"

    ALL = {FROG, PLATYPUS, AARDVARK}

    def __init__(self, **kwargs):
        super().__init__(valid_values=FakeEnumAlt.ALL, **kwargs)


class FakeEnumField(fields.BaseEnumField):
    AUTO_TYPE = FakeEnum()


class FakeStateMachineField(fields.StateMachine):
    ACTIVE = 'ACTIVE'
    PENDING = 'PENDING'
    ERROR = 'ERROR'

    ALLOWED_TRANSITIONS = {
        ACTIVE: {PENDING, ERROR},
        PENDING: {ACTIVE, ERROR},
        ERROR: {
            PENDING,
        },
    }

    _TYPES = (ACTIVE, PENDING, ERROR)

    def __init__(self, **kwargs):
        super().__init__(self._TYPES, **kwargs)


class FakeEnumAltField(fields.BaseEnumField):
    AUTO_TYPE = FakeEnumAlt()


class TestFieldType(test.TestCase):
    def test_get_schema(self):
        self.assertRaises(NotImplementedError, fields.FieldType().get_schema)


class TestField(test.TestCase):
    def setUp(self):
        super().setUp()
        self.field: Any = fields.Field(FakeFieldType())
        self.coerce_good_values: list[tuple[Any, Any]] = [('foo', '*foo*')]
        self.coerce_bad_values: list[Any] = []
        self.to_primitive_values: list[tuple[Any, Any]] = [('foo', '!foo!')]
        self.from_primitive_values: list[tuple[Any, Any]] = [('!foo!', 'foo')]

    def test_coerce_good_values(self):
        for in_val, out_val in self.coerce_good_values:
            self.assertEqual(out_val, self.field.coerce(None, 'attr', in_val))

    def test_coerce_bad_values(self):
        for in_val in self.coerce_bad_values:
            self.assertRaises(
                (TypeError, ValueError),
                self.field.coerce,
                None,
                'attr',
                in_val,
            )

    def test_to_primitive(self):
        for in_val, prim_val in self.to_primitive_values:
            self.assertEqual(
                prim_val, self.field.to_primitive('obj', 'attr', in_val)
            )

    def test_from_primitive(self):
        class AnObject(obj_base.VersionedObject):
            fields = {
                'intfield': fields.IntegerField(),
            }

        for prim_val, out_val in self.from_primitive_values:
            obj = AnObject(intfield=5)
            self.assertEqual(
                out_val, self.field.from_primitive(obj, 'attr', prim_val)
            )

    def test_stringify(self):
        self.assertEqual('123', self.field.stringify(123))


class TestSchema(test.TestCase):
    def setUp(self):
        super().setUp()
        self.field = fields.Field(
            FakeFieldType(), nullable=True, default='', read_only=False
        )

    def test_get_schema(self):
        self.assertEqual(
            {'type': ['foo', 'null'], 'default': '', 'readonly': False},
            self.field.get_schema(),
        )


class TestString(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.StringField()
        self.coerce_good_values = [
            ('foo', 'foo'),
            (1, '1'),
            (1.0, '1.0'),
            (True, 'True'),
        ]
        self.coerce_bad_values = [None]
        self.to_primitive_values = self.coerce_good_values[0:1]
        self.from_primitive_values = self.coerce_good_values[0:1]

    def test_stringify(self):
        self.assertEqual("'123'", self.field.stringify(123))

    def test_fieldtype_get_schema(self):
        self.assertEqual({'type': ['string']}, self.field._type.get_schema())

    def test_get_schema(self):
        self.assertEqual(
            {'type': ['string'], 'readonly': False}, self.field.get_schema()
        )


class TestSensitiveString(TestString):
    def setUp(self):
        super().setUp()
        self.field = fields.SensitiveStringField()

    def test_stringify(self):
        payload = """{'admin_password':'mypassword'}"""
        expected = """'{'admin_password':'***'}'"""
        self.assertEqual(expected, self.field.stringify(payload))


class TestVersionPredicate(TestString):
    def setUp(self):
        super().setUp()
        self.field = fields.VersionPredicateField()
        self.coerce_good_values = [
            ('>=1.0', '>=1.0'),
            ('>1.0', '>1.0'),
            ('==1.1', '==1.1'),
            ('<=1.1.0', '<=1.1.0'),
            ('<1.1.0', '<1.1.0'),
        ]
        self.coerce_bad_values = ['1', 'foo', 1.0, '1.0', '=1.0']
        self.to_primitive_values = self.coerce_good_values[0:1]
        self.from_primitive_values = self.coerce_good_values[0:1]


class TestMACAddress(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.MACAddressField()
        self.coerce_good_values = [
            ('c6:df:11:a5:c8:5d', 'c6:df:11:a5:c8:5d'),
            ('C6:DF:11:A5:C8:5D', 'c6:df:11:a5:c8:5d'),
            ('c6:df:11:a5:c8:5d', 'c6:df:11:a5:c8:5d'),
            ('C6:DF:11:A5:C8:5D', 'c6:df:11:a5:c8:5d'),
        ]
        self.coerce_bad_values = [
            'C6:DF:11:A5:C8',  # Too short
            'C6:DF:11:A5:C8:5D:D7',  # Too long
            'C6:DF:11:A5:C8:KD',  # Bad octal
            1123123,  # Number
            {},  # dict
        ]
        self.to_primitive_values = self.coerce_good_values[0:1]
        self.from_primitive_values = self.coerce_good_values[0:1]

    def test_get_schema(self):
        schema = self.field.get_schema()
        self.assertEqual(['string'], schema['type'])
        self.assertEqual(False, schema['readonly'])
        pattern = schema['pattern']
        for _, valid_val in self.coerce_good_values:
            self.assertRegex(valid_val, pattern)
        invalid_vals = [
            x for x in self.coerce_bad_values if isinstance(x, str)
        ]
        for invalid_val in invalid_vals:
            self.assertNotRegex(invalid_val, pattern)


class TestPCIAddress(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.PCIAddressField()
        self.coerce_good_values = [
            ('0000:02:00.0', '0000:02:00.0'),
            ('FFFF:FF:1F.7', 'ffff:ff:1f.7'),
            ('fFfF:fF:1F.7', 'ffff:ff:1f.7'),
        ]
        self.coerce_bad_values = [
            '000:02:00.0',  # Too short
            '00000:02:00.0',  # Too long
            'FFFF:FF:2F.7',  # Bad slot
            'FFFF:GF:1F.7',  # Bad octal
            1123123,  # Number
            {},  # dict
        ]
        self.to_primitive_values = self.coerce_good_values[0:1]
        self.from_primitive_values = self.coerce_good_values[0:1]

    def test_get_schema(self):
        schema = self.field.get_schema()
        self.assertEqual(['string'], schema['type'])
        self.assertEqual(False, schema['readonly'])
        pattern = schema['pattern']
        for _, valid_val in self.coerce_good_values:
            self.assertRegex(valid_val, pattern)
        invalid_vals = [
            x for x in self.coerce_bad_values if isinstance(x, str)
        ]
        for invalid_val in invalid_vals:
            self.assertNotRegex(invalid_val, pattern)


class TestUUID(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.UUIDField()
        self.coerce_good_values = [
            (
                'da66a411-af0e-4829-9b67-475017ddd152',
                'da66a411-af0e-4829-9b67-475017ddd152',
            ),
            (
                'da66a411af0e48299b67475017ddd152',
                'da66a411af0e48299b67475017ddd152',
            ),
            (
                'DA66A411-AF0E-4829-9B67-475017DDD152',
                'DA66A411-AF0E-4829-9B67-475017DDD152',
            ),
            (
                'DA66A411AF0E48299b67475017DDD152',
                'DA66A411AF0E48299b67475017DDD152',
            ),
            # These values are included to ensure there is not change in
            # behaviour - only when we can remove the old UUID behaviour can
            #  we add these to the "self.coerce_bad_values" list
            ('da66a411-af0e-4829-9b67', 'da66a411-af0e-4829-9b67'),
            (
                'da66a411-af0e-4829-9b67-475017ddd152548999',
                'da66a411-af0e-4829-9b67-475017ddd152548999',
            ),
            (
                'da66a411-af0e-4829-9b67-475017ddz152',
                'da66a411-af0e-4829-9b67-475017ddz152',
            ),
            ('fake_uuid', 'fake_uuid'),
            ('fake_uāid', 'fake_uāid'),
            (
                b'fake_u\xe1id'.decode('latin_1'),
                b'fake_u\xe1id'.decode('latin_1'),
            ),
            ('1', '1'),
            (1, '1'),
        ]
        self.to_primitive_values = self.coerce_good_values[0:1]
        self.from_primitive_values = self.coerce_good_values[0:1]

    @mock.patch('warnings.warn')
    def test_coerce_good_values(self, mock_warn):
        super().test_coerce_good_values()
        mock_warn.assert_has_calls(
            [mock.call(mock.ANY, FutureWarning)] * 8,
        )

    def test_validation_warning_can_be_escalated_to_exception(self):
        warnings.filterwarnings(action='error')
        self.assertRaises(
            FutureWarning, self.field.coerce, 'obj', 'attr', 'not a uuid'
        )

    def test_get_schema(self):
        field = fields.UUIDField()
        schema = field.get_schema()
        self.assertEqual(['string'], schema['type'])
        self.assertEqual(False, schema['readonly'])
        pattern = schema['pattern']
        for _, valid_val in self.coerce_good_values[:4]:
            self.assertRegex(valid_val, pattern)
        invalid_vals = [
            x for x in self.coerce_bad_values if isinstance(x, str)
        ]
        for invalid_val in invalid_vals:
            self.assertNotRegex(invalid_val, pattern)


class TestBaseEnum(TestField):
    def setUp(self):
        super().setUp()
        self.field = FakeEnumField()
        self.coerce_good_values = [
            ('frog', 'frog'),
            ('platypus', 'platypus'),
            ('alligator', 'alligator'),
        ]
        self.coerce_bad_values = ['aardvark', 'wookie']
        self.to_primitive_values = self.coerce_good_values[0:1]
        self.from_primitive_values = self.coerce_good_values[0:1]

    def test_stringify(self):
        self.assertEqual("'platypus'", self.field.stringify('platypus'))

    def test_stringify_invalid(self):
        self.assertRaises(ValueError, self.field.stringify, 'aardvark')

    def test_fingerprint(self):
        # Notes(yjiang5): make sure changing valid_value will be detected
        # in test_objects.test_versions
        field1 = FakeEnumField()
        field2 = FakeEnumAltField()
        self.assertNotEqual(str(field1), str(field2))

    def test_valid_values(self):
        self.assertEqual(self.field.valid_values, FakeEnum.ALL)

    def test_valid_values_keeps_type(self):
        self.assertIsInstance(self.field.valid_values, tuple)
        self.assertIsInstance(FakeEnumAltField().valid_values, set)


class TestEnum(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.EnumField(valid_values=['foo', 'bar', 1, True])
        self.coerce_good_values = [('foo', 'foo'), (1, '1'), (True, 'True')]
        self.coerce_bad_values = ['boo', 2, False]
        self.to_primitive_values = self.coerce_good_values[0:1]
        self.from_primitive_values = self.coerce_good_values[0:1]

    def test_stringify(self):
        self.assertEqual("'foo'", self.field.stringify('foo'))

    def test_stringify_invalid(self):
        self.assertRaises(ValueError, self.field.stringify, '123')

    def test_fieldtype_get_schema(self):
        self.assertEqual(
            {'type': ['string'], 'enum': ["foo", "bar", 1, True]},
            self.field._type.get_schema(),
        )

    def test_get_schema(self):
        self.assertEqual(
            {
                'type': ['string'],
                'enum': ["foo", "bar", 1, True],
                'readonly': False,
            },
            self.field.get_schema(),
        )

    def test_fingerprint(self):
        # Notes(yjiang5): make sure changing valid_value will be detected
        # in test_objects.test_versions
        field1 = fields.EnumField(valid_values=['foo', 'bar'])
        field2 = fields.EnumField(valid_values=['foo', 'bar1'])
        self.assertNotEqual(str(field1), str(field2))

    def test_missing_valid_values(self):
        self.assertRaises(
            exception.EnumRequiresValidValuesError, fields.EnumField, None
        )

    def test_empty_valid_values(self):
        self.assertRaises(
            exception.EnumRequiresValidValuesError, fields.EnumField, []
        )

    def test_non_iterable_valid_values(self):
        self.assertRaises(
            exception.EnumValidValuesInvalidError, fields.EnumField, True
        )

    def test_enum_subclass_check(self):
        def _test():
            class BrokenEnumField(fields.BaseEnumField):
                # this is intentionally using the wrong type
                AUTO_TYPE = int  # type: ignore[assignment]

            BrokenEnumField()

        self.assertRaises(exception.EnumFieldInvalid, _test)


class TestStateMachine(TestField):
    def test_good_transitions(self):
        @obj_base.VersionedObjectRegistry.register
        class AnObject(obj_base.VersionedObject):
            fields = {
                'status': FakeStateMachineField(),
            }

        obj = AnObject()

        obj.status = FakeStateMachineField.ACTIVE
        obj.status = FakeStateMachineField.PENDING
        obj.status = FakeStateMachineField.ERROR
        obj.status = FakeStateMachineField.PENDING
        obj.status = FakeStateMachineField.ACTIVE

    def test_bad_transitions(self):
        @obj_base.VersionedObjectRegistry.register
        class AnObject(obj_base.VersionedObject):
            fields = {
                'status': FakeStateMachineField(),
            }

        obj = AnObject(status='ERROR')

        try:
            obj.status = FakeStateMachineField.ACTIVE
        except ValueError as ex:
            self.assertEqual(
                'AnObject.status is not allowed to transition out '
                'of \'ERROR\' state to \'ACTIVE\' state, choose from '
                '[\'PENDING\']',
                str(ex),
            )
        else:
            raise Exception('Invalid transition failed to raise error')

    def test_bad_initial_value(self):
        @obj_base.VersionedObjectRegistry.register
        class AnObject(obj_base.VersionedObject):
            fields = {
                'status': FakeStateMachineField(),
            }

        obj = AnObject()

        with testtools.ExpectedException(ValueError):
            obj.status = "FOO"

    def test_bad_updated_value(self):
        @obj_base.VersionedObjectRegistry.register
        class AnObject(obj_base.VersionedObject):
            fields = {
                'status': FakeStateMachineField(),
            }

        obj = AnObject()

        with testtools.ExpectedException(ValueError):
            obj.status = FakeStateMachineField.ACTIVE
            obj.status = "FOO"


class TestInteger(TestField):
    def setUp(self):
        super(TestField, self).setUp()
        self.field = fields.IntegerField()
        self.coerce_good_values = [(1, 1), ('1', 1)]
        self.coerce_bad_values = ['foo', None]
        self.to_primitive_values = self.coerce_good_values[0:1]
        self.from_primitive_values = self.coerce_good_values[0:1]

    def test_fieldtype_get_schema(self):
        self.assertEqual({'type': ['integer']}, self.field._type.get_schema())

    def test_get_schema(self):
        self.assertEqual(
            {'type': ['integer'], 'readonly': False}, self.field.get_schema()
        )


class TestNonNegativeInteger(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.NonNegativeIntegerField()
        self.coerce_good_values = [(1, 1), ('1', 1)]
        self.coerce_bad_values = ['-2', '4.2', 'foo', None]
        self.to_primitive_values = self.coerce_good_values[0:1]
        self.from_primitive_values = self.coerce_good_values[0:1]

    def test_get_schema(self):
        self.assertEqual(
            {'type': ['integer'], 'readonly': False, 'minimum': 0},
            self.field.get_schema(),
        )


class TestFloat(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.FloatField()
        self.coerce_good_values = [(1.1, 1.1), ('1.1', 1.1)]
        self.coerce_bad_values = ['foo', None]
        self.to_primitive_values = self.coerce_good_values[0:1]
        self.from_primitive_values = self.coerce_good_values[0:1]

    def test_fieldtype_get_schema(self):
        self.assertEqual({'type': ['number']}, self.field._type.get_schema())

    def test_get_schema(self):
        self.assertEqual(
            {'type': ['number'], 'readonly': False}, self.field.get_schema()
        )


class TestNonNegativeFloat(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.NonNegativeFloatField()
        self.coerce_good_values = [(1.1, 1.1), ('1.1', 1.1)]
        self.coerce_bad_values = ['-4.2', 'foo', None]
        self.to_primitive_values = self.coerce_good_values[0:1]
        self.from_primitive_values = self.coerce_good_values[0:1]

    def test_get_schema(self):
        self.assertEqual(
            {'type': ['number'], 'readonly': False, 'minimum': 0},
            self.field.get_schema(),
        )


class TestBoolean(TestField):
    def setUp(self):
        super(TestField, self).setUp()
        self.field = fields.BooleanField()
        self.coerce_good_values = [
            (True, True),
            (False, False),
            (1, True),
            ('foo', True),
            (0, False),
            ('', False),
        ]
        self.coerce_bad_values = []
        self.to_primitive_values = self.coerce_good_values[0:2]
        self.from_primitive_values = self.coerce_good_values[0:2]

    def test_fieldtype_get_schema(self):
        self.assertEqual({'type': ['boolean']}, self.field._type.get_schema())

    def test_get_schema(self):
        self.assertEqual(
            {'type': ['boolean'], 'readonly': False}, self.field.get_schema()
        )


class TestFlexibleBoolean(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.FlexibleBooleanField()
        self.coerce_good_values = [
            (True, True),
            (False, False),
            ("true", True),
            ("false", False),
            ("t", True),
            ("f", False),
            ("yes", True),
            ("no", False),
            ("y", True),
            ("n", False),
            ("on", True),
            ("off", False),
            (1, True),
            (0, False),
            ('frog', False),
            ('', False),
        ]
        self.coerce_bad_values = []
        self.to_primitive_values = self.coerce_good_values[0:2]
        self.from_primitive_values = self.coerce_good_values[0:2]


class TestDateTime(TestField):
    def setUp(self):
        super().setUp()
        self.dt = datetime.datetime(1955, 11, 5, tzinfo=datetime.UTC)
        self.field = fields.DateTimeField()
        self.coerce_good_values = [
            (self.dt, self.dt),
            (_utils.isotime(self.dt), self.dt),
        ]
        self.coerce_bad_values = [1, 'foo']
        self.to_primitive_values = [(self.dt, _utils.isotime(self.dt))]
        self.from_primitive_values = [(_utils.isotime(self.dt), self.dt)]

    def test_stringify(self):
        self.assertEqual(
            '1955-11-05T18:00:00Z',
            self.field.stringify(
                datetime.datetime(1955, 11, 5, 18, 0, 0, tzinfo=datetime.UTC)
            ),
        )

    def test_get_schema(self):
        self.assertEqual(
            {'type': ['string'], 'format': 'date-time', 'readonly': False},
            self.field.get_schema(),
        )


class TestDateTimeNoTzinfo(TestField):
    def setUp(self):
        super().setUp()
        self.dt = datetime.datetime(1955, 11, 5)
        self.field = fields.DateTimeField(tzinfo_aware=False)
        self.coerce_good_values = [
            (self.dt, self.dt),
            (_utils.isotime(self.dt), self.dt),
        ]
        self.coerce_bad_values = [1, 'foo']
        self.to_primitive_values = [(self.dt, _utils.isotime(self.dt))]
        self.from_primitive_values = [
            (
                _utils.isotime(self.dt),
                self.dt,
            )
        ]

    def test_stringify(self):
        self.assertEqual(
            '1955-11-05T18:00:00Z',
            self.field.stringify(datetime.datetime(1955, 11, 5, 18, 0, 0)),
        )


class TestDict(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.Field(fields.Dict(FakeFieldType()))
        self.coerce_good_values = [
            ({'foo': 'bar'}, {'foo': '*bar*'}),
            ({'foo': 1}, {'foo': '*1*'}),
        ]
        self.coerce_bad_values = [{1: 'bar'}, 'foo']
        self.to_primitive_values = [({'foo': 'bar'}, {'foo': '!bar!'})]
        self.from_primitive_values = [({'foo': '!bar!'}, {'foo': 'bar'})]

    def test_stringify(self):
        self.assertEqual("{key=val}", self.field.stringify({'key': 'val'}))

    def test_get_schema(self):
        self.assertEqual(
            {
                'type': ['object'],
                'additionalProperties': {'readonly': False, 'type': ['foo']},
                'readonly': False,
            },
            self.field.get_schema(),
        )


class TestDictOfStrings(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.DictOfStringsField()
        self.coerce_good_values = [
            ({'foo': 'bar'}, {'foo': 'bar'}),
            ({'foo': 1}, {'foo': '1'}),
        ]
        self.coerce_bad_values = [{1: 'bar'}, {'foo': None}, 'foo']
        self.to_primitive_values = [({'foo': 'bar'}, {'foo': 'bar'})]
        self.from_primitive_values = [({'foo': 'bar'}, {'foo': 'bar'})]

    def test_stringify(self):
        self.assertEqual("{key='val'}", self.field.stringify({'key': 'val'}))


class TestDictOfIntegers(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.DictOfIntegersField()
        self.coerce_good_values = [
            ({'foo': '42'}, {'foo': 42}),
            ({'foo': 4.2}, {'foo': 4}),
        ]
        self.coerce_bad_values = [
            {1: 'bar'},
            {'foo': 'boo'},
            'foo',
            {'foo': None},
        ]
        self.to_primitive_values = [({'foo': 42}, {'foo': 42})]
        self.from_primitive_values = [({'foo': 42}, {'foo': 42})]

    def test_stringify(self):
        self.assertEqual("{key=42}", self.field.stringify({'key': 42}))


class TestDictOfStringsNone(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.DictOfNullableStringsField()
        self.coerce_good_values = [
            ({'foo': 'bar'}, {'foo': 'bar'}),
            ({'foo': 1}, {'foo': '1'}),
            ({'foo': None}, {'foo': None}),
        ]
        self.coerce_bad_values = [{1: 'bar'}, 'foo']
        self.to_primitive_values = [({'foo': 'bar'}, {'foo': 'bar'})]
        self.from_primitive_values = [({'foo': 'bar'}, {'foo': 'bar'})]

    def test_stringify(self):
        self.assertEqual(
            "{k2=None,key='val'}",
            self.field.stringify({'k2': None, 'key': 'val'}),
        )


class TestListOfDictOfNullableStringsField(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.ListOfDictOfNullableStringsField()
        self.coerce_good_values = [
            (
                [{'f': 'b', 'f1': 'b1'}, {'f2': 'b2'}],
                [{'f': 'b', 'f1': 'b1'}, {'f2': 'b2'}],
            ),
            ([{'f': 1}, {'f1': 'b1'}], [{'f': '1'}, {'f1': 'b1'}]),
            ([{'foo': None}], [{'foo': None}]),
        ]
        self.coerce_bad_values = [[{1: 'a'}], ['ham', 1], ['eggs']]
        self.to_primitive_values = [
            (
                [{'f': 'b'}, {'f1': 'b1'}, {'f2': None}],
                [{'f': 'b'}, {'f1': 'b1'}, {'f2': None}],
            )
        ]
        self.from_primitive_values = [
            (
                [{'f': 'b'}, {'f1': 'b1'}, {'f2': None}],
                [{'f': 'b'}, {'f1': 'b1'}, {'f2': None}],
            )
        ]

    def test_stringify(self):
        self.assertEqual(
            "[{f=None,f1='b1'},{f2='b2'}]",
            self.field.stringify([{'f': None, 'f1': 'b1'}, {'f2': 'b2'}]),
        )


class TestList(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.Field(fields.List(FakeFieldType()))
        self.coerce_good_values = [(['foo', 'bar'], ['*foo*', '*bar*'])]
        self.coerce_bad_values = ['foo']
        self.to_primitive_values = [(['foo'], ['!foo!'])]
        self.from_primitive_values = [(['!foo!'], ['foo'])]

    def test_stringify(self):
        self.assertEqual('[123]', self.field.stringify([123]))

    def test_fieldtype_get_schema(self):
        self.assertEqual(
            {'type': ['array'], 'items': {'type': ['foo'], 'readonly': False}},
            self.field._type.get_schema(),
        )

    def test_get_schema(self):
        self.assertEqual(
            {
                'type': ['array'],
                'items': {'type': ['foo'], 'readonly': False},
                'readonly': False,
            },
            self.field.get_schema(),
        )


class TestListOfStrings(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.ListOfStringsField()
        self.coerce_good_values = [(['foo', 'bar'], ['foo', 'bar'])]
        self.coerce_bad_values = ['foo']
        self.to_primitive_values = [(['foo'], ['foo'])]
        self.from_primitive_values = [(['foo'], ['foo'])]

    def test_stringify(self):
        self.assertEqual("['abc']", self.field.stringify(['abc']))


class TestSetOfStrings(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.SetOfStringsField()
        self.coerce_good_values = [
            ({'foo', 'bar'}, {'foo', 'bar'}),
            ({'foo', 1}, {'foo', '1'}),
        ]
        self.coerce_bad_values = [['foo']]
        self.to_primitive_values = [({'foo'}, tuple(['foo']))]
        self.from_primitive_values = [(tuple(['foo']), {'foo'})]

    def test_stringify(self):
        self.assertEqual("set(['abc'])", self.field.stringify({'abc'}))


class TestListOfListsOfStrings(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.ListOfListsOfStringsField()
        self.coerce_good_values = [
            ([['foo', 'bar']], [['foo', 'bar']]),
            ([['foo', 1]], [['foo', '1']]),
        ]
        self.coerce_bad_values = ['foo', [['foo', None]]]
        self.to_primitive_values = [([['foo']], [['foo']])]
        self.from_primitive_values = [([['foo']], [['foo']])]

    def test_stringify(self):
        self.assertEqual("[['abc']]", self.field.stringify([['abc']]))


class TestDictOfListOfStrings(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.DictOfListOfStringsField()
        self.coerce_good_values = [
            ({'foo': ['1', '2']}, {'foo': ['1', '2']}),
            ({'foo': [1]}, {'foo': ['1']}),
        ]
        self.coerce_bad_values = [{'foo': [None, None]}, 'foo']
        self.to_primitive_values = [({'foo': ['1', '2']}, {'foo': ['1', '2']})]
        self.from_primitive_values = [
            ({'foo': ['1', '2']}, {'foo': ['1', '2']})
        ]

    def test_stringify(self):
        self.assertEqual(
            "{foo=['1','2']}", self.field.stringify({'foo': ['1', '2']})
        )


class TestDictOfSetOfIntegers(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.DictOfSetOfIntegersField()
        self.coerce_good_values = [({'foo': {'1', 2}}, {'foo': {1, 2}})]
        self.coerce_bad_values = [{'foo': {'bar'}}]
        self.to_primitive_values = [({'foo': {1}}, {'foo': tuple([1])})]
        self.from_primitive_values = [({'foo': tuple([1])}, {'foo': {1}})]

    def test_stringify(self):
        self.assertEqual('{foo=set([1])}', self.field.stringify({'foo': {1}}))


class TestListOfEnum(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.ListOfEnumField(valid_values=['foo', 'bar'])
        self.coerce_good_values = [(['foo', 'bar'], ['foo', 'bar'])]
        self.coerce_bad_values = ['foo', ['foo', 'bar1']]
        self.to_primitive_values = [(['foo'], ['foo'])]
        self.from_primitive_values = [(['foo'], ['foo'])]

    def test_stringify(self):
        self.assertEqual("['foo']", self.field.stringify(['foo']))

    def test_stringify_invalid(self):
        self.assertRaises(ValueError, self.field.stringify, '[abc]')

    def test_fingerprint(self):
        # Notes(yjiang5): make sure changing valid_value will be detected
        # in test_objects.test_versions
        field1 = fields.ListOfEnumField(valid_values=['foo', 'bar'])
        field2 = fields.ListOfEnumField(valid_values=['foo', 'bar1'])
        self.assertNotEqual(str(field1), str(field2))


class TestSet(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.Field(fields.Set(FakeFieldType()))
        self.coerce_good_values = [({'foo', 'bar'}, {'*foo*', '*bar*'})]
        self.coerce_bad_values = [['foo'], {'foo': 'bar'}]
        self.to_primitive_values = [({'foo'}, tuple(['!foo!']))]
        self.from_primitive_values = [(tuple(['!foo!']), {'foo'})]

    def test_stringify(self):
        self.assertEqual('set([123])', self.field.stringify({123}))

    def test_get_schema(self):
        self.assertEqual(
            {
                'type': ['array'],
                'uniqueItems': True,
                'items': {'type': ['foo'], 'readonly': False},
                'readonly': False,
            },
            self.field.get_schema(),
        )


class TestSetOfIntegers(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.SetOfIntegersField()
        self.coerce_good_values = [({'1', 2}, {1, 2})]
        self.coerce_bad_values = [{'foo'}]
        self.to_primitive_values = [({1}, tuple([1]))]
        self.from_primitive_values = [(tuple([1]), {1})]

    def test_stringify(self):
        self.assertEqual('set([1,2])', self.field.stringify({1, 2}))

    def test_repr(self):
        self.assertEqual(
            "Set(default=<class 'oslo_versionedobjects.fields."
            "UnspecifiedDefault'>,nullable=False)",
            repr(self.field),
        )
        self.assertEqual(
            "Set(default=set([]),nullable=False)",
            repr(fields.SetOfIntegersField(default=set())),
        )
        self.assertEqual(
            "Set(default=set([1,a]),nullable=False)",
            repr(fields.SetOfIntegersField(default={1, 'a'})),  # type: ignore
        )


class TestListOfSetsOfIntegers(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.ListOfSetsOfIntegersField()
        self.coerce_good_values = [([{'1', 2}, {3, '4'}], [{1, 2}, {3, 4}])]
        self.coerce_bad_values = [[{'foo'}]]
        self.to_primitive_values = [([{1}], [tuple([1])])]
        self.from_primitive_values = [([tuple([1])], [{1}])]

    def test_stringify(self):
        self.assertEqual('[set([1,2])]', self.field.stringify([{1, 2}]))


class TestListOfIntegers(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.ListOfIntegersField()
        self.coerce_good_values = [(['1', 2], [1, 2]), ([1, 2], [1, 2])]
        self.coerce_bad_values = [['foo']]
        self.to_primitive_values = [([1], [1])]
        self.from_primitive_values = [([1], [1])]

    def test_stringify(self):
        self.assertEqual('[[1, 2]]', self.field.stringify([[1, 2]]))


class TestListOfUUIDField(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.ListOfUUIDField()
        self.uuid1 = '6b2097ea-d0e3-44dd-b131-95472b3ea8fd'
        self.uuid2 = '478c193d-2533-4e71-ab2b-c7683f67d7f9'
        self.coerce_good_values = [
            ([self.uuid1, self.uuid2], [self.uuid1, self.uuid2])
        ]
        # coerce_bad_values is intentionally ignored since the UUID field
        # allows non-UUID values for now. See TestUUIDField for examples.
        self.to_primitive_values = [([self.uuid1], [self.uuid1])]
        self.from_primitive_values = [([self.uuid1], [self.uuid1])]

    def test_stringify(self):
        self.assertEqual(
            f'[{self.uuid1},{self.uuid2}]',
            self.field.stringify([self.uuid1, self.uuid2]),
        )


class TestLocalMethods(test.TestCase):
    @mock.patch.object(obj_base.LOG, 'exception')
    def test__make_class_properties_setter_value_error(self, mock_log):
        @obj_base.VersionedObjectRegistry.register
        class AnObject(obj_base.VersionedObject):
            fields = {
                'intfield': fields.IntegerField(),
            }

        self.assertRaises(ValueError, AnObject, intfield='badvalue')
        self.assertFalse(mock_log.called)

    @mock.patch.object(obj_base.LOG, 'exception')
    def test__make_class_properties_setter_setattr_fails(self, mock_log):
        @obj_base.VersionedObjectRegistry.register
        class AnObject(obj_base.VersionedObject):
            fields = {
                'intfield': fields.IntegerField(),
            }

        # We want the setattr() call in _make_class_properties.setter() to
        # raise an exception
        with mock.patch.object(obj_base, '_get_attrname') as mock_attr:
            mock_attr.return_value = '__class__'
            self.assertRaises(TypeError, AnObject, intfield=2)
            mock_attr.assert_called_once_with('intfield')
            mock_log.assert_called_once_with(
                mock.ANY, {'attr': 'AnObject.intfield'}
            )


class TestObject(TestField):
    def setUp(self):
        super().setUp()

        @obj_base.VersionedObjectRegistry.register
        class TestableObject(obj_base.VersionedObject):
            fields = {
                'uuid': fields.StringField(),
            }

            def __eq__(self, value):
                # NOTE(danms): Be rather lax about this equality thing to
                # satisfy the assertEqual() in test_from_primitive(). We
                # just want to make sure the right type of object is re-created
                return value.__class__.__name__ == TestableObject.__name__

        class OtherTestableObject(obj_base.VersionedObject):
            pass

        test_inst = TestableObject()
        self._test_cls = TestableObject
        self.field = fields.Field(fields.Object('TestableObject'))
        self.coerce_good_values = [(test_inst, test_inst)]
        self.coerce_bad_values = [OtherTestableObject(), 1, 'foo']
        self.to_primitive_values = [(test_inst, test_inst.obj_to_primitive())]
        self.from_primitive_values = [
            (test_inst.obj_to_primitive(), test_inst),
            (test_inst, test_inst),
        ]

    def test_stringify(self):
        obj = self._test_cls(uuid='fake-uuid')
        self.assertEqual(
            'TestableObject(fake-uuid)', self.field.stringify(obj)
        )

    def test_from_primitive(self):
        @obj_base.VersionedObjectRegistry.register
        class TestFakeObject(obj_base.VersionedObject):
            OBJ_PROJECT_NAMESPACE = 'fake-project'

        @obj_base.VersionedObjectRegistry.register
        class TestBar(TestFakeObject, obj_base.ComparableVersionedObject):
            fields = {
                'name': fields.StringField(),
            }

        @obj_base.VersionedObjectRegistry.register
        class TestFoo(TestFakeObject, obj_base.ComparableVersionedObject):
            fields = {
                'name': fields.StringField(),
                'bar': fields.ObjectField('TestBar', nullable=True),
            }

        bar = TestBar(name='bar')
        foo = TestFoo(name='foo', bar=bar)
        from_primitive_values = [(foo.obj_to_primitive(), foo), (foo, foo)]

        for prim_val, out_val in from_primitive_values:
            self.assertEqual(
                out_val, self.field.from_primitive(foo, 'attr', prim_val)
            )

    def test_inheritance(self):
        # We need a whole lot of classes in a hierarchy to
        # test subclass recognition for the Object field
        class TestAnimal(obj_base.VersionedObject):
            pass

        class TestMammal(TestAnimal):
            pass

        class TestReptile(TestAnimal):
            pass

        # We'll use this to create a diamond in the
        # class hierarchy
        class TestPet(TestAnimal):
            pass

        # Non-versioned object mixin
        class TestScary:
            pass

        class TestCrocodile(TestReptile, TestPet, TestScary):
            pass

        class TestPig(TestMammal):
            pass

        class TestDog(TestMammal, TestPet):
            pass

        # Some fictional animals
        wolfy = TestDog()  # Terminator-2
        ticktock = TestCrocodile()  # Peter Pan
        babe = TestPig()  # Babe

        # The various classes
        animals = fields.Object('TestAnimal', subclasses=True)
        mammals = fields.Object('TestMammal', subclasses=True)
        reptiles = fields.Object('TestReptile', subclasses=True)
        pets = fields.Object('TestPet', subclasses=True)
        pigs = fields.Object('TestPig', subclasses=True)
        dogs = fields.Object('TestDog', subclasses=True)
        crocs = fields.Object('TestCrocodile', subclasses=True)

        self.assertEqual(
            [
                "TestDog",
                "TestMammal",
                "TestPet",
                "TestAnimal",
                "VersionedObject",
            ],
            fields.Object._get_all_obj_names(wolfy),
        )

        self.assertEqual(
            [
                "TestCrocodile",
                "TestReptile",
                "TestPet",
                "TestAnimal",
                "VersionedObject",
            ],
            fields.Object._get_all_obj_names(ticktock),
        )

        self.assertEqual(
            ["TestPig", "TestMammal", "TestAnimal", "VersionedObject"],
            fields.Object._get_all_obj_names(babe),
        )

        # When stringifying we should see the subclass object name
        # not the base class object name
        self.assertEqual("TestDog", animals.stringify(wolfy))
        self.assertEqual("TestCrocodile", animals.stringify(ticktock))
        self.assertEqual("TestPig", animals.stringify(babe))

        # Everything is an animal
        self.assertEqual(wolfy, animals.coerce(None, "animal", wolfy))
        self.assertEqual(ticktock, animals.coerce(None, "animal", ticktock))
        self.assertEqual(babe, animals.coerce(None, "animal", babe))

        # crocodiles are not mammals
        self.assertEqual(wolfy, mammals.coerce(None, "animal", wolfy))
        self.assertRaises(ValueError, mammals.coerce, None, "animal", ticktock)
        self.assertEqual(babe, mammals.coerce(None, "animal", babe))

        # dogs and pigs are not reptiles
        self.assertRaises(ValueError, reptiles.coerce, None, "animal", wolfy)
        self.assertEqual(ticktock, reptiles.coerce(None, "animal", ticktock))
        self.assertRaises(ValueError, reptiles.coerce, None, "animal", babe)

        # pigs are not pets, but crocodiles (!) & dogs are
        self.assertEqual(wolfy, pets.coerce(None, "animal", wolfy))
        self.assertEqual(ticktock, pets.coerce(None, "animal", ticktock))
        self.assertRaises(ValueError, pets.coerce, None, "animal", babe)

        # Only dogs are dogs
        self.assertEqual(wolfy, dogs.coerce(None, "animal", wolfy))
        self.assertRaises(ValueError, dogs.coerce, None, "animal", ticktock)
        self.assertRaises(ValueError, dogs.coerce, None, "animal", babe)

        # Only crocs are crocs
        self.assertRaises(ValueError, crocs.coerce, None, "animal", wolfy)
        self.assertEqual(ticktock, crocs.coerce(None, "animal", ticktock))
        self.assertRaises(ValueError, crocs.coerce, None, "animal", babe)

        # Only pigs are pigs
        self.assertRaises(ValueError, pigs.coerce, None, "animal", ticktock)
        self.assertRaises(ValueError, pigs.coerce, None, "animal", wolfy)
        self.assertEqual(babe, pigs.coerce(None, "animal", babe))

    def test_coerce_bad_value_primitive_type(self):
        # Tests that the ValueError has the primitive type in it's message.
        ex = self.assertRaises(
            ValueError, self.field.coerce, 'obj', 'attr', [{}]
        )
        self.assertEqual(
            'An object of type TestableObject is required '
            'in field attr, not a list',
            str(ex),
        )

    def test_get_schema(self):
        self.assertEqual(
            {
                'properties': {
                    'versioned_object.changes': {
                        'items': {'type': 'string'},
                        'type': 'array',
                    },
                    'versioned_object.data': {
                        'description': 'fields of TestableObject',
                        'properties': {
                            'uuid': {'readonly': False, 'type': ['string']}
                        },
                        'required': ['uuid'],
                        'type': 'object',
                    },
                    'versioned_object.name': {'type': 'string'},
                    'versioned_object.namespace': {'type': 'string'},
                    'versioned_object.version': {'type': 'string'},
                },
                'readonly': False,
                'required': [
                    'versioned_object.namespace',
                    'versioned_object.name',
                    'versioned_object.version',
                    'versioned_object.data',
                ],
                'type': ['object'],
            },
            self.field.get_schema(),
        )


class TestIPAddress(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.IPAddressField()
        self.coerce_good_values = [
            ('1.2.3.4', netaddr.IPAddress('1.2.3.4')),
            ('::1', netaddr.IPAddress('::1')),
            (netaddr.IPAddress('::1'), netaddr.IPAddress('::1')),
        ]
        self.coerce_bad_values = ['1-2', 'foo']
        self.to_primitive_values = [
            (netaddr.IPAddress('1.2.3.4'), '1.2.3.4'),
            (netaddr.IPAddress('::1'), '::1'),
        ]
        self.from_primitive_values = [
            ('1.2.3.4', netaddr.IPAddress('1.2.3.4')),
            ('::1', netaddr.IPAddress('::1')),
        ]


class TestIPAddressV4(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.IPV4AddressField()
        self.coerce_good_values = [
            ('1.2.3.4', netaddr.IPAddress('1.2.3.4')),
            (netaddr.IPAddress('1.2.3.4'), netaddr.IPAddress('1.2.3.4')),
        ]
        self.coerce_bad_values = ['1-2', 'foo', '::1']
        self.to_primitive_values = [(netaddr.IPAddress('1.2.3.4'), '1.2.3.4')]
        self.from_primitive_values = [
            ('1.2.3.4', netaddr.IPAddress('1.2.3.4'))
        ]

    def test_get_schema(self):
        self.assertEqual(
            {'type': ['string'], 'readonly': False, 'format': 'ipv4'},
            self.field.get_schema(),
        )


class TestIPAddressV6(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.IPV6AddressField()
        self.coerce_good_values = [
            ('::1', netaddr.IPAddress('::1')),
            (netaddr.IPAddress('::1'), netaddr.IPAddress('::1')),
        ]
        self.coerce_bad_values = ['1.2', 'foo', '1.2.3.4']
        self.to_primitive_values = [(netaddr.IPAddress('::1'), '::1')]
        self.from_primitive_values = [('::1', netaddr.IPAddress('::1'))]

    def test_get_schema(self):
        self.assertEqual(
            {'type': ['string'], 'readonly': False, 'format': 'ipv6'},
            self.field.get_schema(),
        )


class TestIPV4AndV6Address(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.IPV4AndV6Address()
        self.coerce_good_values = [
            ('::1', netaddr.IPAddress('::1')),
            (netaddr.IPAddress('::1'), netaddr.IPAddress('::1')),
            ('1.2.3.4', netaddr.IPAddress('1.2.3.4')),
            (netaddr.IPAddress('1.2.3.4'), netaddr.IPAddress('1.2.3.4')),
        ]
        self.coerce_bad_values = ['1-2', 'foo']
        self.to_primitive_values = [
            (netaddr.IPAddress('::1'), '::1'),
            (netaddr.IPAddress('1.2.3.4'), '1.2.3.4'),
        ]
        self.from_primitive_values = [
            ('::1', netaddr.IPAddress('::1')),
            ('1.2.3.4', netaddr.IPAddress('1.2.3.4')),
        ]

    def test_get_schema(self):
        self.assertEqual(
            {
                'oneOf': [
                    {'format': 'ipv4', 'type': ['string']},
                    {'format': 'ipv6', 'type': ['string']},
                ]
            },
            self.field.get_schema(),
        )


class TestIPNetwork(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.IPNetworkField()
        self.coerce_good_values = [
            ('::1/0', netaddr.IPNetwork('::1/0')),
            ('1.2.3.4/24', netaddr.IPNetwork('1.2.3.4/24')),
            (netaddr.IPNetwork('::1/32'), netaddr.IPNetwork('::1/32')),
        ]
        self.coerce_bad_values = ['foo']
        self.to_primitive_values = [(netaddr.IPNetwork('::1/0'), '::1/0')]
        self.from_primitive_values = [('::1/0', netaddr.IPNetwork('::1/0'))]


class TestIPV4Network(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.IPV4NetworkField()
        self.coerce_good_values = [
            ('1.2.3.4/24', netaddr.IPNetwork('1.2.3.4/24'))
        ]
        self.coerce_bad_values = ['foo', '::1/32']
        self.to_primitive_values = [
            (netaddr.IPNetwork('1.2.3.4/24'), '1.2.3.4/24')
        ]
        self.from_primitive_values = [
            ('1.2.3.4/24', netaddr.IPNetwork('1.2.3.4/24'))
        ]

    def test_get_schema(self):
        schema = self.field.get_schema()
        self.assertEqual(['string'], schema['type'])
        self.assertEqual(False, schema['readonly'])
        pattern = schema['pattern']
        for _, valid_val in self.coerce_good_values:
            self.assertRegex(str(valid_val), pattern)
        invalid_vals = [x for x in self.coerce_bad_values]
        for invalid_val in invalid_vals:
            self.assertNotRegex(str(invalid_val), pattern)


class TestIPV6Network(TestField):
    def setUp(self):
        super().setUp()
        self.field = fields.IPV6NetworkField()
        self.coerce_good_values = [
            ('::1/0', netaddr.IPNetwork('::1/0')),
            (netaddr.IPNetwork('::1/32'), netaddr.IPNetwork('::1/32')),
        ]
        self.coerce_bad_values = ['foo', '1.2.3.4/24']
        self.to_primitive_values = [(netaddr.IPNetwork('::1/0'), '::1/0')]
        self.from_primitive_values = [('::1/0', netaddr.IPNetwork('::1/0'))]

    def test_get_schema(self):
        schema = self.field.get_schema()
        self.assertEqual(['string'], schema['type'])
        self.assertEqual(False, schema['readonly'])
        pattern = schema['pattern']
        for _, valid_val in self.coerce_good_values:
            self.assertRegex(str(valid_val), pattern)
        invalid_vals = [x for x in self.coerce_bad_values]
        for invalid_val in invalid_vals:
            self.assertNotRegex(str(invalid_val), pattern)


class FakeCounter:
    def __init__(self):
        self.n = 0

    def __iter__(self):
        return self

    def __next__(self):
        if self.n <= 4:
            self.n += 1
            return self.n
        else:
            raise StopIteration


class TestListTypes(test.TestCase):
    def test_regular_list(self):
        fields.List(fields.Integer()).coerce(None, '', [1, 2])

    def test_non_iterable(self):
        self.assertRaises(
            ValueError, fields.List(fields.Integer()).coerce, None, '', 2
        )

    def test_string_iterable(self):
        self.assertRaises(
            ValueError,
            fields.List(fields.Integer()).coerce,
            None,
            None,
            'hello',
        )

    def test_mapping_iterable(self):
        self.assertRaises(
            ValueError,
            fields.List(fields.Integer()).coerce,
            None,
            None,
            {'a': 1, 'b': 2},
        )

    def test_iter_class(self):
        fields.List(fields.Integer()).coerce(None, '', FakeCounter())
