# (c) cavaliba.com - tests / data / fields

from django.test import RequestFactory, TestCase

import app_home.cache as cache
from app_data.data import Instance
from app_data.fieldtypes.field_date import FieldDate
from app_data.fieldtypes.field_datetime import FieldDatetime
from app_data.fieldtypes.field_enumerate import FieldEnumerate
from app_data.fieldtypes.field_float import FieldFloat
from app_data.fieldtypes.field_int import FieldInt
from app_data.fieldtypes.field_ipv4 import FieldIPV4
from app_data.fieldtypes.field_permission import FieldPermission
from app_data.fieldtypes.field_schema import FieldSchema
from app_data.fieldtypes.field_string import FieldString
from app_data.fieldtypes.field_text import FieldText
from app_data.fieldtypes.field_time import FieldTime
from tests.helper import add_instance, add_schema

ENUMERATE_TEST_CONTENT = (
    '- value: "A"\n'
    '  widget: "green_circle"\n'
    "  enumstring: string AAA\n"
    "  enumint: int 111\n"
    "  enumfloat: float 111.1\n"
    "  enumboolean: boolean no\n"
    "  enumdate: date 2026-01-15\n"
    '- value: "B"\n'
    '  widget: "yellow_circle"\n'
    '- value: "C"\n'
    '  widget: "orange_circle"\n'
)


class TestDataFieldTypes(TestCase):
    def setUp(self):
        cache.clear()

        add_instance(
            classname="_enumerate",
            keyname="enumerate_test",
            fields={
                "content": ENUMERATE_TEST_CONTENT,
            },
        )

        add_schema(
            classname="test0",
            field_definition={
                "mystring": {"dataformat": "string", "displayname": "MyString"},
            },
        )
        add_instance(classname="test0", keyname="test0-01")
        add_instance(classname="test0", keyname="test0-02")
        add_instance(classname="test0", keyname="test0-03")

        add_schema(
            classname="test1",
            field_definition={
                "mystring": {"dataformat": "string", "displayname": "MyString"},
                "mystring_default": {
                    "dataformat": "string",
                    "displayname": "MyString Default",
                    "default_value": "hello",
                },
                "myint": {"dataformat": "int", "displayname": "MyInt"},
                "myint_default": {
                    "dataformat": "int",
                    "displayname": "MyInt Default",
                    "default_value": "11",
                },
                "myint_default_multi": {
                    "dataformat": "int",
                    "displayname": "MyInt Default Multi",
                    "default_value": "11 22",
                    "cardinal_max": 0,
                },
                "myfloat": {"dataformat": "float", "displayname": "MyFloat"},
                "myfloat_default": {
                    "dataformat": "float",
                    "displayname": "MyFloat Default",
                    "default_value": "11.11",
                },
                "myfloat_default_multi": {
                    "dataformat": "float",
                    "displayname": "MyFloat Default Multi",
                    "default_value": "11.11 22.22",
                    "cardinal_max": 0,
                },
                "myboolean": {"dataformat": "boolean", "displayname": "MyBoolean"},
                "myboolean_default_true": {
                    "dataformat": "boolean",
                    "displayname": "MyBoolean Default True",
                    "default_value": "True",
                },
                "myboolean_default_false": {
                    "dataformat": "boolean",
                    "displayname": "MyBoolean Default False",
                    "default_value": "False",
                },
                "mydate": {"dataformat": "date", "displayname": "MyDate"},
                "myipv4": {"dataformat": "ipv4", "displayname": "MyIPV4"},
                "my_schema_test0": {
                    "dataformat": "schema",
                    "displayname": "Schema Test0",
                    "dataformat_ext": "test0",
                },
                "my_schema_test0_multi": {
                    "dataformat": "schema",
                    "displayname": "Schema Test0 Multi",
                    "dataformat_ext": "test0",
                    "cardinal_max": 0,
                },
                "my_schema_self": {
                    "dataformat": "schema",
                    "displayname": "Schema Self",
                    "dataformat_ext": "test1",
                },
                "my_enumerate_abcde": {
                    "dataformat": "enumerate",
                    "displayname": "Enumerate ABCDE",
                    "dataformat_ext": "enumerate_test",
                },
                "my_enumerate_abcde_multi": {
                    "dataformat": "enumerate",
                    "displayname": "Enumerate Multi ABCDE",
                    "dataformat_ext": "enumerate_test",
                    "cardinal_max": 0,
                },
                "my_enumerate_subfields": {
                    "dataformat": "enumerate",
                    "displayname": "Enumerate Subfields",
                    "dataformat_ext": "enumerate_test",
                },
            },
        )
        add_instance(
            classname="test1",
            keyname="test1-01",
            fields={
                "mystring": "data for test1-01",
                "myint": 11,
                "myfloat": 27.19,
                "myboolean": True,
                "mydate": "2024-11-01",
                "myipv4": "10.1.2.3",
                "my_schema_test0": "test0-01",
                "my_schema_test0_multi": ["test0-02", "test0-03"],
                "my_schema_self": "test1-03",
                "my_enumerate_abcde": "A",
                "my_enumerate_abcde_multi": ["A", "B", "C"],
                "my_enumerate_subfields": "A",
            },
        )
        add_instance(classname="test1", keyname="test1-03")

        add_schema(
            classname="test2",
            field_definition={
                "test1": {
                    "dataformat": "schema",
                    "displayname": "Test1 Object",
                    "dataformat_ext": "test1 mystring myint myfloat myboolean",
                },
            },
        )
        add_instance(classname="test2", keyname="test2-01", fields={"test1": "test1-01"})

        add_schema(
            classname="test3",
            field_definition={
                "test2": {
                    "dataformat": "schema",
                    "displayname": "Test2 Object",
                    "dataformat_ext": "test2",
                },
                "_test1": {
                    "dataformat": "external",
                    "displayname": "Test1 (external)",
                    "dataformat_ext": "test2 test1 mystring myint myfloat myboolean",
                },
            },
        )
        add_instance(classname="test3", keyname="test3-01", fields={"test2": "test2-01"})

        add_schema(
            classname="test4",
            field_definition={
                "test3": {
                    "dataformat": "schema",
                    "displayname": "Test3 Object",
                    "dataformat_ext": "test3",
                },
                "_test1": {
                    "dataformat": "external",
                    "displayname": "Test1 (external)",
                    "dataformat_ext": "test3 test1 displayname is_enabled mystring myint myfloat myboolean",
                },
            },
        )
        add_instance(classname="test4", keyname="test4-01", fields={"test3": "test3-01"})

    def test_field_values(self):
        instance = Instance.from_keyname(classname="test1", keyname="test1-01", expand=False)
        self.assertIsNotNone(instance)
        self.assertEqual(instance.fields["mystring"].get_value(), ["data for test1-01"])
        self.assertEqual(instance.fields["myint"].get_value(), [11])
        self.assertEqual(instance.fields["myfloat"].get_value(), [27.19])
        self.assertEqual(instance.fields["myboolean"].get_value(), [True])
        self.assertEqual(instance.fields["mydate"].get_value(), ["2024-11-01"])
        self.assertEqual(instance.fields["myipv4"].get_value(), ["10.1.2.3"])
        self.assertEqual(instance.fields["my_schema_test0"].get_value(), ["test0-01"])
        self.assertEqual(
            instance.fields["my_schema_test0_multi"].get_value(), ["test0-02", "test0-03"]
        )
        self.assertEqual(instance.fields["my_schema_self"].get_value(), ["test1-03"])

    def test_default_value(self):
        instance = Instance(classname="test1")
        self.assertEqual(instance.fields["mystring_default"].get_value(), ["hello"])
        self.assertEqual(instance.fields["myint_default"].get_value(), [11])
        self.assertEqual(instance.fields["myint_default_multi"].get_value(), [11, 22])
        self.assertEqual(instance.fields["myfloat_default"].get_value(), [11.11])
        self.assertEqual(instance.fields["myfloat_default_multi"].get_value(), [11.11, 22.22])

    def test_boolean(self):
        instance = Instance(classname="test1")

        a = instance.fields["myboolean_default_true"].get_first_value()
        self.assertEqual(type(a), bool)
        self.assertTrue(a)

        b = instance.fields["myboolean_default_false"].get_first_value()
        self.assertEqual(type(b), bool)
        self.assertFalse(b)

    def test_external_no_expand(self):
        instance = Instance.from_keyname(classname="test4", keyname="test4-01", expand=False)
        self.assertIsNotNone(instance)
        self.assertIsNotNone(instance.fields["_test1"])
        self.assertFalse("test1" in instance.fields)

    def test_external_expand(self):
        instance = Instance.from_keyname(classname="test4", keyname="test4-01", expand=True)
        self.assertIsNotNone(instance)
        self.assertIsNotNone(instance.fields["_test1"])
        self.assertIsNotNone(instance.fields["test1"])
        self.assertIsNotNone(instance.fields["test1__mystring"])
        self.assertIsNotNone(instance.fields["test1__myint"])
        self.assertIsNotNone(instance.fields["test1__myfloat"])
        self.assertIsNotNone(instance.fields["test1__myboolean"])
        self.assertEqual(instance.fields["test1__myint"].get_value(), [11])
        self.assertEqual(instance.fields["test1__myfloat"].get_value(), [27.19])
        self.assertEqual(instance.fields["test1__myboolean"].get_value(), [True])
        self.assertEqual(instance.fields["test1__mystring"].get_value(), ["data for test1-01"])

    def test_enumerate_no_expand(self):
        instance = Instance.from_keyname(classname="test1", keyname="test1-01", expand=False)
        self.assertIsNotNone(instance)
        self.assertIsNotNone(instance.fields["my_enumerate_abcde"])
        self.assertEqual(instance.fields["my_enumerate_abcde"].get_value(), ["A"])
        self.assertIsNotNone(instance.fields["my_enumerate_abcde_multi"])
        self.assertTrue("A" in instance.fields["my_enumerate_abcde_multi"].get_value())
        self.assertTrue("B" in instance.fields["my_enumerate_abcde_multi"].get_value())
        self.assertTrue("C" in instance.fields["my_enumerate_abcde_multi"].get_value())

        self.assertIsNotNone(instance.fields["my_enumerate_subfields"])
        self.assertEqual(instance.fields["my_enumerate_subfields"].get_value(), ["A"])
        self.assertFalse("my_enumerate_subfields__widget" in instance.fields)
        self.assertFalse("my_enumerate_subfields__enumstring" in instance.fields)
        self.assertFalse("my_enumerate_subfields__enumint" in instance.fields)
        self.assertFalse("my_enumerate_subfields__enumfloat" in instance.fields)
        self.assertFalse("my_enumerate_subfields__enumboolean" in instance.fields)

    def test_enumerate_expand(self):
        instance = Instance.from_keyname(classname="test1", keyname="test1-01", expand=True)
        self.assertIsNotNone(instance)
        self.assertIsNotNone(instance.fields["my_enumerate_abcde"])
        self.assertEqual(instance.fields["my_enumerate_abcde"].get_value(), ["A"])
        self.assertIsNotNone(instance.fields["my_enumerate_abcde_multi"])
        self.assertTrue("A" in instance.fields["my_enumerate_abcde_multi"].get_value())
        self.assertTrue("B" in instance.fields["my_enumerate_abcde_multi"].get_value())
        self.assertTrue("C" in instance.fields["my_enumerate_abcde_multi"].get_value())

        self.assertIsNotNone(instance.fields["my_enumerate_subfields"])
        self.assertEqual(instance.fields["my_enumerate_subfields"].get_value(), ["A"])
        self.assertTrue("my_enumerate_subfields__widget" in instance.fields)
        self.assertTrue("my_enumerate_subfields__enumstring" in instance.fields)
        self.assertTrue("my_enumerate_subfields__enumint" in instance.fields)
        self.assertTrue("my_enumerate_subfields__enumfloat" in instance.fields)
        self.assertTrue("my_enumerate_subfields__enumboolean" in instance.fields)
        self.assertTrue("my_enumerate_subfields__enumdate" in instance.fields)

        self.assertEqual(instance.fields["my_enumerate_subfields__enumint"].get_value(), [111])
        self.assertEqual(instance.fields["my_enumerate_subfields__enumfloat"].get_value(), [111.1])
        self.assertEqual(instance.fields["my_enumerate_subfields__enumstring"].get_value(), ["AAA"])
        self.assertEqual(
            instance.fields["my_enumerate_subfields__enumboolean"].get_value(), [False]
        )
        self.assertEqual(
            instance.fields["my_enumerate_subfields__enumdate"].get_value(), ["2026-01-15"]
        )
        self.assertEqual(
            instance.fields["my_enumerate_subfields__widget"].get_value(), ["&#x1F7E2;"]
        )

    def test_int_valid(self):
        fieldschema = {"cardinal_min": 0, "cardinal_max": 1, "default_value": ""}

        f = FieldInt(fieldname="data", fieldschema=fieldschema, alljson={"data": [33]})
        self.assertEqual(f.is_valid(), True)
        f = FieldInt(fieldname="data", fieldschema=fieldschema, alljson={"data": [-3]})
        self.assertEqual(f.is_valid(), True)

        f = FieldInt(fieldname="data", fieldschema=fieldschema, alljson={"data": ["a"]})
        self.assertEqual(f.is_valid(), False)
        f = FieldInt(fieldname="data", fieldschema=fieldschema, alljson={"data": [33.4]})
        self.assertEqual(f.is_valid(), False)

    def test_float_valid(self):
        fieldschema = {"cardinal_min": 0, "cardinal_max": 1, "default_value": ""}

        f = FieldFloat(fieldname="data", fieldschema=fieldschema, alljson={"data": [33.2]})
        self.assertEqual(f.is_valid(), True)

        f = FieldFloat(fieldname="data", fieldschema=fieldschema, alljson={"data": ["a"]})
        self.assertEqual(f.is_valid(), False)

    def test_ipv4_valid(self):
        fieldschema = {"cardinal_min": 0, "cardinal_max": 1, "default_value": ""}

        f = FieldIPV4(fieldname="myip", fieldschema=fieldschema, alljson={"myip": ["10.1.2.3"]})
        self.assertEqual(f.is_valid(), True)
        f = FieldIPV4(fieldname="myip", fieldschema=fieldschema, alljson={"myip": ["0.0.0.0"]})
        self.assertEqual(f.is_valid(), True)
        f = FieldIPV4(
            fieldname="myip", fieldschema=fieldschema, alljson={"myip": ["255.255.255.255"]}
        )
        self.assertEqual(f.is_valid(), True)

        f = FieldIPV4(fieldname="myip", fieldschema=fieldschema, alljson={"myip": ["10.1.2.3/24"]})
        self.assertEqual(f.is_valid(), False)
        f = FieldIPV4(fieldname="myip", fieldschema=fieldschema, alljson={"myip": ["10.1.2.256"]})
        self.assertEqual(f.is_valid(), False)
        f = FieldIPV4(fieldname="myip", fieldschema=fieldschema, alljson={"myip": ["10.1.2"]})
        self.assertEqual(f.is_valid(), False)
        f = FieldIPV4(fieldname="myip", fieldschema=fieldschema, alljson={"myip": ["10.1.2.3.4"]})
        self.assertEqual(f.is_valid(), False)
        f = FieldIPV4(fieldname="myip", fieldschema=fieldschema, alljson={"myip": ["10.1.2.3/33"]})
        self.assertEqual(f.is_valid(), False)


class TestFieldUserMissingUser(TestCase):
    def setUp(self):
        cache.clear()

    def test_user_field_missing_user_shows_placeholder(self):
        add_schema(
            classname="schema_with_user",
            field_definition={
                "owner": {
                    "displayname": "Owner",
                    "dataformat": "user",
                }
            },
        )
        add_instance(
            classname="schema_with_user", keyname="item-01", fields={"owner": "nonexistent-login"}
        )

        instance = Instance.from_keyname(classname="schema_with_user", keyname="item-01")
        self.assertIsNotNone(instance)

        datapoint = instance.fields["owner"].get_datapoint_ui_detail()

        self.assertEqual(len(datapoint["value"]), 1)
        self.assertEqual(datapoint["value"][0]["display"], "?? - nonexistent-login")


class TestFieldSchemaMissingInstance(TestCase):
    def setUp(self):
        cache.clear()

    def test_schema_field_missing_instance_shows_placeholder(self):
        add_schema(classname="target_schema")
        add_schema(
            classname="source_schema",
            field_definition={
                "ref_field": {
                    "displayname": "Reference Field",
                    "dataformat": "schema",
                    "dataformat_ext": "target_schema",
                }
            },
        )
        add_instance(
            classname="source_schema", keyname="source-01", fields={"ref_field": "nonexistent-key"}
        )

        instance = Instance.from_keyname(classname="source_schema", keyname="source-01")
        self.assertIsNotNone(instance)

        datapoint = instance.fields["ref_field"].get_datapoint_ui_detail()

        self.assertEqual(len(datapoint["value"]), 1)
        self.assertEqual(datapoint["value"][0]["display"], "?? - nonexistent-key")


class TestFieldBaseSnapshot(TestCase):
    def test_enumerate_snapshot_normalizes_none_and_empty(self):
        fieldschema = {"dataformat": "enumerate", "dataformat_ext": "some_enum"}
        f_none = FieldEnumerate(
            fieldname="status", fieldschema=fieldschema, alljson={"status": [None]}
        )
        f_empty = FieldEnumerate(
            fieldname="status", fieldschema=fieldschema, alljson={"status": [""]}
        )
        self.assertEqual(f_none.get_snapshot_value(), [])
        self.assertEqual(f_empty.get_snapshot_value(), [])
        self.assertEqual(f_none.get_snapshot_value(), f_empty.get_snapshot_value())

    def test_schema_snapshot_normalizes_none_and_empty(self):
        fieldschema = {"dataformat": "schema", "dataformat_ext": "some_class"}
        f_none = FieldSchema(fieldname="ref", fieldschema=fieldschema, alljson={"ref": [None]})
        f_empty = FieldSchema(fieldname="ref", fieldschema=fieldschema, alljson={"ref": [""]})
        self.assertEqual(f_none.get_snapshot_value(), [])
        self.assertEqual(f_empty.get_snapshot_value(), [])
        self.assertEqual(f_none.get_snapshot_value(), f_empty.get_snapshot_value())

    def test_permission_snapshot_normalizes_none_and_empty(self):
        fieldschema = {"dataformat": "permission"}
        f_none = FieldPermission(
            fieldname="perm", fieldschema=fieldschema, alljson={"perm": [None]}
        )
        f_empty = FieldPermission(fieldname="perm", fieldschema=fieldschema, alljson={"perm": [""]})
        self.assertEqual(f_none.get_snapshot_value(), [])
        self.assertEqual(f_empty.get_snapshot_value(), [])
        self.assertEqual(f_none.get_snapshot_value(), f_empty.get_snapshot_value())

    def test_ipv4_snapshot_normalizes_none_and_empty(self):
        fieldschema = {"dataformat": "ipv4"}
        f_none = FieldIPV4(fieldname="ip", fieldschema=fieldschema, alljson={"ip": [None]})
        f_empty = FieldIPV4(fieldname="ip", fieldschema=fieldschema, alljson={"ip": [""]})
        self.assertEqual(f_none.get_snapshot_value(), [])
        self.assertEqual(f_empty.get_snapshot_value(), [])
        self.assertEqual(f_none.get_snapshot_value(), f_empty.get_snapshot_value())

    def test_ipv4_snapshot_keeps_real_value(self):
        fieldschema = {"dataformat": "ipv4"}
        f = FieldIPV4(fieldname="ip", fieldschema=fieldschema, alljson={"ip": ["10.1.2.3"]})
        self.assertEqual(f.get_snapshot_value(), ["10.1.2.3"])

    def test_merge_import_none_produces_empty_snapshot(self):
        """base Field.merge_import(None) stores [None] via split_composite; snapshot must normalize it."""
        fieldschema = {"dataformat": "ipv4"}
        f = FieldIPV4(fieldname="ip", fieldschema=fieldschema, alljson={"ip": ["10.1.2.3"]})
        f.merge_import(None)
        self.assertEqual(f.value, [None])
        self.assertEqual(f.get_snapshot_value(), [])


class TestFieldStringSnapshot(TestCase):
    FIELDSCHEMA = {"dataformat": "string", "cardinal_min": 0, "cardinal_max": 1}

    def test_snapshot_none_value_normalized_to_empty(self):
        f = FieldString(fieldname="title", fieldschema=self.FIELDSCHEMA, alljson={"title": [None]})
        self.assertEqual(f.get_snapshot_value(), [])

    def test_snapshot_empty_string_value_normalized_to_empty(self):
        f = FieldString(fieldname="title", fieldschema=self.FIELDSCHEMA, alljson={"title": [""]})
        self.assertEqual(f.get_snapshot_value(), [])

    def test_snapshot_missing_value_is_empty(self):
        f = FieldString(fieldname="title", fieldschema=self.FIELDSCHEMA, alljson={})
        self.assertEqual(f.get_snapshot_value(), [])

    def test_snapshot_none_and_empty_string_are_equal(self):
        f_none = FieldString(
            fieldname="title", fieldschema=self.FIELDSCHEMA, alljson={"title": [None]}
        )
        f_empty = FieldString(
            fieldname="title", fieldschema=self.FIELDSCHEMA, alljson={"title": [""]}
        )
        self.assertEqual(f_none.get_snapshot_value(), f_empty.get_snapshot_value())

    def test_snapshot_keeps_real_value(self):
        f = FieldString(
            fieldname="title", fieldschema=self.FIELDSCHEMA, alljson={"title": ["hello"]}
        )
        self.assertEqual(f.get_snapshot_value(), ["hello"])

    def test_snapshot_drops_blank_entries_in_multi_value(self):
        fieldschema = {"dataformat": "string", "cardinal_min": 0, "cardinal_max": 0}
        f = FieldString(
            fieldname="tags", fieldschema=fieldschema, alljson={"tags": ["a", None, "", "b"]}
        )
        self.assertEqual(f.get_snapshot_value(), ["a", "b"])


class TestFieldTextSnapshot(TestCase):
    FIELDSCHEMA = {"dataformat": "text", "cardinal_min": 0, "cardinal_max": 1}

    def test_snapshot_none_value_normalized_to_empty(self):
        f = FieldText(fieldname="note", fieldschema=self.FIELDSCHEMA, alljson={"note": [None]})
        self.assertEqual(f.get_snapshot_value(), [])

    def test_snapshot_empty_string_value_normalized_to_empty(self):
        f = FieldText(fieldname="note", fieldschema=self.FIELDSCHEMA, alljson={"note": [""]})
        self.assertEqual(f.get_snapshot_value(), [])

    def test_snapshot_none_and_empty_string_are_equal(self):
        f_none = FieldText(fieldname="note", fieldschema=self.FIELDSCHEMA, alljson={"note": [None]})
        f_empty = FieldText(fieldname="note", fieldschema=self.FIELDSCHEMA, alljson={"note": [""]})
        self.assertEqual(f_none.get_snapshot_value(), f_empty.get_snapshot_value())

    def test_snapshot_keeps_real_value(self):
        f = FieldText(
            fieldname="note", fieldschema=self.FIELDSCHEMA, alljson={"note": ["hello world"]}
        )
        self.assertEqual(f.get_snapshot_value(), ["hello world"])

    def test_merge_import_none_stores_none(self):
        f = FieldText(fieldname="note", fieldschema=self.FIELDSCHEMA, alljson={"note": ["old"]})
        f.merge_import(None)
        self.assertEqual(f.value, [None])
        self.assertEqual(f.get_snapshot_value(), [])


class TestFieldTimeValid(TestCase):
    FIELDSCHEMA = {"dataformat": "time", "cardinal_min": 0, "cardinal_max": 1}

    def test_valid_time_string_is_valid(self):
        f = FieldTime(fieldname="t", fieldschema=self.FIELDSCHEMA, alljson={"t": ["10:00:00"]})
        self.assertTrue(f.is_valid())

    def test_wrong_length_string_is_invalid(self):
        f = FieldTime(fieldname="t", fieldschema=self.FIELDSCHEMA, alljson={"t": ["10:00"]})
        self.assertFalse(f.is_valid())

    def test_non_time_string_is_invalid(self):
        f = FieldTime(fieldname="t", fieldschema=self.FIELDSCHEMA, alljson={"t": ["not-a-time"]})
        self.assertFalse(f.is_valid())

    def test_right_length_but_unparsable_string_is_invalid(self):
        f = FieldTime(fieldname="t", fieldschema=self.FIELDSCHEMA, alljson={"t": ["aa:bb:cc"]})
        self.assertFalse(f.is_valid())

    def test_empty_value_is_valid(self):
        f = FieldTime(fieldname="t", fieldschema=self.FIELDSCHEMA, alljson={"t": []})
        self.assertTrue(f.is_valid())


class TestFieldTemporalMergeImport(TestCase):
    def test_date_merge_import_list_with_none_does_not_crash(self):
        fieldschema = {"dataformat": "date", "cardinal_max": 0}
        f = FieldDate(fieldname="d", fieldschema=fieldschema, alljson={"d": ["2024-01-01"]})
        f.merge_import(["2024-01-01", None, "2024-02-02"])
        self.assertEqual(f.value, ["2024-01-01", "2024-02-02"])

    def test_datetime_merge_import_list_with_none_does_not_crash(self):
        fieldschema = {"dataformat": "datetime", "cardinal_max": 0}
        f = FieldDatetime(
            fieldname="dt", fieldschema=fieldschema, alljson={"dt": ["2024-01-01 10:00:00"]}
        )
        f.merge_import(["2024-01-01 10:00:00", None, "2024-02-02 11:00:00"])
        self.assertEqual(f.value, ["2024-01-01 10:00:00", "2024-02-02 11:00:00"])

    def test_time_merge_import_list_with_none_does_not_crash(self):
        fieldschema = {"dataformat": "time", "cardinal_max": 0}
        f = FieldTime(fieldname="t", fieldschema=fieldschema, alljson={"t": ["10:00:00"]})
        f.merge_import(["10:00:00", None, "11:00:00"])
        self.assertEqual(f.value, ["10:00:00", "11:00:00"])

    def test_date_merge_import_list_with_int_does_not_crash(self):
        fieldschema = {"dataformat": "date", "cardinal_max": 0}
        f = FieldDate(fieldname="d", fieldschema=fieldschema, alljson={"d": []})
        f.merge_import(["2024-01-01", 20240101])
        self.assertEqual(f.value, ["2024-01-01"])


class TestFieldDatetimeIso8601(TestCase):
    FIELDSCHEMA = {"dataformat": "datetime", "cardinal_max": 0}

    def test_merge_import_canonical_unchanged(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        f.merge_import("2024-01-01 10:00:00")
        self.assertEqual(f.value, ["2024-01-01 10:00:00"])

    def test_merge_import_iso_t_separator(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        f.merge_import("2024-01-01T10:00:00")
        self.assertEqual(f.value, ["2024-01-01 10:00:00"])

    def test_merge_import_iso_z_suffix_converts_to_utc(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        f.merge_import("2024-01-01T10:00:00Z")
        self.assertEqual(f.value, ["2024-01-01 10:00:00"])

    def test_merge_import_iso_offset_converts_to_utc(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        f.merge_import("2024-01-01T12:00:00+02:00")
        self.assertEqual(f.value, ["2024-01-01 10:00:00"])

    def test_merge_import_iso_millisec_truncated(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        f.merge_import("2024-01-01T10:00:00.900Z")
        self.assertEqual(f.value, ["2024-01-01 10:00:00"])

    def test_merge_import_iso_millisec_offset_combined(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        f.merge_import("2024-01-01T12:00:00.500+02:00")
        self.assertEqual(f.value, ["2024-01-01 10:00:00"])

    def test_merge_import_invalid_string_dropped(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        f.merge_import("not-a-date")
        self.assertEqual(f.value, [])

    def test_merge_import_date_only_rejected(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        f.merge_import("2024-01-01")
        self.assertEqual(f.value, [])

    def test_merge_import_list_mixed_formats(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        f.merge_import(
            [
                "2024-01-01 10:00:00",
                "2024-01-02T10:00:00Z",
                "not-a-date",
                None,
            ]
        )
        self.assertEqual(f.value, ["2024-01-01 10:00:00", "2024-01-02 10:00:00"])

    def test_is_valid_accepts_both_formats(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        f.value = ["2024-01-01 10:00:00", "2024-01-02T10:00:00Z"]
        self.assertTrue(f.is_valid())

    def test_is_valid_rejects_bad_format(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        f.value = ["2024-01-01T99:99:99"]
        self.assertFalse(f.is_valid())

    def test_merge_request_normalizes_iso_input(self):
        f = FieldDatetime(fieldname="dt", fieldschema=self.FIELDSCHEMA, alljson={})
        rf = RequestFactory()
        request = rf.post("/", {"dt": "2024-01-01T10:00:00Z"})
        f.merge_request(request)
        self.assertEqual(f.value, ["2024-01-01 10:00:00"])
