import dataclasses
import inspect
import pathlib
import re
import sys
from importlib import import_module
from pathlib import Path
from types import LambdaType
from typing import Any, Callable, Dict, List, Optional, Tuple, Union

import pytest
from pytest import fixture, mark, param, raises

from omegaconf import (
    MISSING,
    AnyNode,
    Container,
    DictConfig,
    KeyValidationError,
    ListConfig,
    MissingMandatoryValue,
    OmegaConf,
    ReadonlyConfigError,
    ValidationError,
    _utils,
)
from omegaconf.errors import ConfigKeyError, InterpolationToMissingValueError
from tests import Color, Enum1, User, warns_dict_subclass_deprecated


@fixture(
    params=[
        param("tests.structured_conf.data.dataclasses", id="dataclasses"),
        param(
            "tests.structured_conf.data.dataclasses_pre_311",
            id="dataclasses_pre_311",
            marks=mark.skipif(
                sys.version_info >= (3, 11),
                reason="python >= 3.11 does not support mutable default dataclass arguments",
            ),
        ),
        param("tests.structured_conf.data.attr_classes", id="attr_classes"),
    ],
)
def module(request: Any) -> Any:
    return import_module(request.param)


class EnumConfigAssignments:
    legal = [
        (Color.RED, Color.RED),
        (Color.GREEN, Color.GREEN),
        (Color.BLUE, Color.BLUE),
        ("RED", Color.RED),
        ("GREEN", Color.GREEN),
        ("BLUE", Color.BLUE),
        ("Color.RED", Color.RED),
        ("Color.GREEN", Color.GREEN),
        ("Color.BLUE", Color.BLUE),
        (1, Color.RED),
        (2, Color.GREEN),
        (3, Color.BLUE),
    ]
    illegal = ["foo", True, b"RED", False, 4, 1.0, Path("hello.txt")]


class IntegersConfigAssignments:
    legal = [("10", 10), ("-10", -10), 100, 0, 1]
    illegal = [
        "foo",
        1.0,
        float("inf"),
        b"123",
        float("nan"),
        Color.BLUE,
        True,
        Path("hello.txt"),
    ]


class StringConfigAssignments:
    legal = [
        "10",
        "-10",
        "foo",
        "",
        (Color.BLUE, "Color.BLUE"),
        (Path("hello.txt"), "hello.txt"),
    ]
    illegal = [b"binary"]


class BytesConfigAssignments:
    legal = [b"binary"]
    illegal = ["foo", 10, Color.BLUE, 10.1, True, Path("hello.txt")]


class PathConfigAssignments:
    legal = [Path("hello.txt"), ("hello.txt", Path("hello.txt"))]
    illegal = [10, Color.BLUE, 10.1, True, b"binary"]


class FloatConfigAssignments:
    legal = [
        ("inf", float("inf")),
        ("-inf", float("-inf")),
        (10, 10.0),
        (10.1, 10.1),
        ("10.2", 10.2),
        ("10e-3", 10e-3),
    ]
    illegal = ["foo", True, False, b"10.1", Color.BLUE, Path("hello.txt")]


class BoolConfigAssignments:
    legal = [
        (True, True),
        ("Y", True),
        ("true", True),
        ("Yes", True),
        ("On", True),
        ("1", True),
        (100, True),
        (False, False),
        ("N", False),
        ("false", False),
        ("No", False),
        ("Off", False),
        ("0", False),
        (0, False),
    ]
    illegal = [100.0, b"binary", Color.BLUE, Path("hello.txt")]


class AnyTypeConfigAssignments:
    legal = [True, False, 10, 10.0, b"binary", "foobar", Color.BLUE, Path("hello.txt")]

    illegal: Any = []


class TestConfigs:
    def test_nested_config_is_none(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.NestedWithNone)
        assert cfg == {"plugin": None}
        assert OmegaConf.get_type(cfg, "plugin") is None
        assert _utils.get_type_hint(cfg, "plugin") == Optional[module.Plugin]

    def test_nested_config(self, module: Any) -> None:
        def validate(cfg: DictConfig) -> None:
            assert cfg == {
                "default_value": {
                    "with_default": 10,
                    "null_default": None,
                    "mandatory_missing": "???",
                    "interpolation": "${value_at_root}",
                },
                "user_provided_default": {
                    "with_default": 42,
                    "null_default": None,
                    "mandatory_missing": "???",
                    "interpolation": "${value_at_root}",
                },
                "value_at_root": 1000,
            }

            with raises(ValidationError):
                cfg.user_provided_default = 10

            with raises(ValidationError):
                cfg.default_value = 10

            # assign subclass
            cfg.default_value = module.NestedSubclass()
            assert cfg.default_value == {
                "additional": 20,
                "with_default": 10,
                "null_default": None,
                "mandatory_missing": "???",
                "interpolation": "${value_at_root}",
            }

            # assign original ref type back
            cfg.default_value = module.Nested()
            assert cfg.default_value == module.Nested()

        conf1 = OmegaConf.structured(module.NestedConfig(default_value=module.Nested()))
        validate(conf1)

    def test_nested_config2(self, module: Any) -> None:
        def validate(cfg: DictConfig) -> None:
            assert cfg == {
                "default_value": "???",
                "user_provided_default": {
                    "with_default": 42,
                    "null_default": None,
                    "mandatory_missing": "???",
                    "interpolation": "${value_at_root}",
                },
                "value_at_root": 1000,
            }

            with raises(ValidationError):
                cfg.user_provided_default = 10

            with raises(ValidationError):
                cfg.default_value = 10

            # assign subclass
            cfg.default_value = module.NestedSubclass()
            assert cfg.default_value == {
                "additional": 20,
                "with_default": 10,
                "null_default": None,
                "mandatory_missing": "???",
                "interpolation": "${value_at_root}",
            }

            # assign original ref type back
            cfg.default_value = module.Nested()
            assert cfg.default_value == module.Nested()

        conf1 = OmegaConf.structured(module.NestedConfig)
        validate(conf1)

    def test_value_without_a_default(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.NoDefaultValue)
        assert OmegaConf.is_missing(cfg, "no_default")

        OmegaConf.structured(module.NoDefaultValue(no_default=10)) == {"no_default": 10}

    def test_union_errors(self, module: Any) -> None:
        with raises(ValueError):
            OmegaConf.structured(module.UnionError)

    def test_config_with_list(self, module: Any) -> None:
        def validate(cfg: DictConfig) -> None:
            assert cfg == {"list1": [1, 2, 3], "list2": [1, 2, 3], "missing": MISSING}
            with raises(ValidationError):
                cfg.list1[1] = "foo"

            assert OmegaConf.is_missing(cfg, "missing")

        conf1 = OmegaConf.structured(module.ConfigWithList)
        validate(conf1)

        conf2 = OmegaConf.structured(module.ConfigWithList())
        validate(conf2)

    def test_config_with_list_nondefault_values(self, module: Any) -> None:
        conf1 = OmegaConf.structured(module.ConfigWithList(list1=[4, 5, 6]))
        assert conf1.list1 == [4, 5, 6]

        conf2 = OmegaConf.structured(module.ConfigWithList(list1=MISSING))
        assert OmegaConf.is_missing(conf2, "list1")

    def test_assignment_to_nested_structured_config(self, module: Any) -> None:
        conf = OmegaConf.structured(module.NestedConfig)
        with raises(ValidationError):
            conf.default_value = 10

        conf.default_value = module.Nested()

    def test_assignment_to_structured_inside_dict_config(self, module: Any) -> None:
        conf = OmegaConf.create(
            {"val": DictConfig(module.Nested, ref_type=module.Nested)}
        )
        with raises(ValidationError):
            conf.val = 10

    def test_config_with_dict(self, module: Any) -> None:
        def validate(cfg: DictConfig) -> None:
            assert cfg == {"dict1": {"foo": "bar"}, "missing": MISSING}
            assert OmegaConf.is_missing(cfg, "missing")

        conf1 = OmegaConf.structured(module.ConfigWithDict)
        validate(conf1)

        conf2 = OmegaConf.structured(module.ConfigWithDict())
        validate(conf2)

    def test_config_with_dict_nondefault_values(self, module: Any) -> None:
        conf1 = OmegaConf.structured(module.ConfigWithDict(dict1={"baz": "qux"}))
        assert conf1.dict1 == {"baz": "qux"}

        conf2 = OmegaConf.structured(module.ConfigWithDict(dict1=MISSING))
        assert OmegaConf.is_missing(conf2, "dict1")

    def test_structured_config_struct_behavior(self, module: Any) -> None:
        def validate(cfg: DictConfig) -> None:
            assert not OmegaConf.is_struct(cfg)
            with raises(AttributeError):
                # noinspection PyStatementEffect
                cfg.foo

            cfg.dict1.foo = 10
            assert cfg.dict1.foo == 10

            # setting struct False on a specific typed node opens it up even though it's
            # still typed
            OmegaConf.set_struct(cfg, False)
            cfg.foo = 20
            assert cfg.foo == 20

        conf = OmegaConf.structured(module.ConfigWithDict)
        validate(conf)
        conf = OmegaConf.structured(module.ConfigWithDict())
        validate(conf)

    @mark.parametrize(
        "tested_type,assignment_data, init_dict",
        [
            # Use class to build config
            ("BoolConfig", BoolConfigAssignments, {}),
            ("IntegersConfig", IntegersConfigAssignments, {}),
            ("FloatConfig", FloatConfigAssignments, {}),
            ("BytesConfig", BytesConfigAssignments, {}),
            ("PathConfig", PathConfigAssignments, {}),
            ("StringConfig", StringConfigAssignments, {}),
            ("EnumConfig", EnumConfigAssignments, {}),
            # Use instance to build config
            ("BoolConfig", BoolConfigAssignments, {"with_default": False}),
            ("IntegersConfig", IntegersConfigAssignments, {"with_default": 42}),
            ("FloatConfig", FloatConfigAssignments, {"with_default": 42.0}),
            ("BytesConfig", BytesConfigAssignments, {"with_default": b"bin"}),
            ("PathConfig", PathConfigAssignments, {"with_default": Path("file.txt")}),
            ("StringConfig", StringConfigAssignments, {"with_default": "fooooooo"}),
            ("EnumConfig", EnumConfigAssignments, {"with_default": Color.BLUE}),
            ("AnyTypeConfig", AnyTypeConfigAssignments, {}),
        ],
    )
    def test_field_with_default_value(
        self,
        module: Any,
        tested_type: str,
        init_dict: Dict[str, Any],
        assignment_data: Any,
    ) -> None:
        input_class = getattr(module, tested_type)

        def validate(input_: Any, expected: Any) -> None:
            conf = OmegaConf.structured(input_)
            # Test access
            assert conf.with_default == expected.with_default
            assert conf.null_default is None
            # Test that accessing a variable without a default value
            # results in a MissingMandatoryValue exception
            with raises(MissingMandatoryValue):
                # noinspection PyStatementEffect
                conf.mandatory_missing

            # Test interpolation preserves type and value
            assert type(conf.with_default) == type(conf.interpolation)  # noqa E721
            assert conf.with_default == conf.interpolation

            # Test that assignment of illegal values
            for illegal_value in assignment_data.illegal:
                with raises(ValidationError):
                    conf.with_default = illegal_value

                with raises(ValidationError):
                    conf.null_default = illegal_value

                with raises(ValidationError):
                    conf.mandatory_missing = illegal_value

            # Test assignment of legal values
            for legal_value in assignment_data.legal:
                expected_data = legal_value
                if isinstance(legal_value, tuple):
                    expected_data = legal_value[1]
                    legal_value = legal_value[0]
                conf.with_default = legal_value
                conf.null_default = legal_value
                conf.mandatory_missing = legal_value

                msg = "Error: {} : {}".format(input_class.__name__, legal_value)
                assert conf.with_default == expected_data, msg
                assert conf.null_default == expected_data, msg
                assert conf.mandatory_missing == expected_data, msg

        validate(input_class, input_class())
        validate(input_class(**init_dict), input_class(**init_dict))

    @mark.parametrize(
        "input_init, expected_init",
        [
            # attr class as class
            (None, {}),
            # attr class object with custom values
            ({"int_default": 30}, {"int_default": 30}),
            # dataclass as class
            (None, {}),
            # dataclass as object with custom values
            ({"int_default": 30}, {"int_default": 30}),
        ],
    )
    def test_untyped(self, module: Any, input_init: Any, expected_init: Any) -> None:
        input_ = module.AnyTypeConfig
        expected = input_(**expected_init)
        if input_init is not None:
            input_ = input_(**input_init)

        conf = OmegaConf.structured(input_)
        assert conf.null_default == expected.null_default
        assert conf.int_default == expected.int_default
        assert conf.float_default == expected.float_default
        assert conf.str_default == expected.str_default
        assert conf.bool_default == expected.bool_default
        # yes, this is weird.
        assert "mandatory_missing" in conf.keys() and "mandatory_missing" not in conf

        with raises(MissingMandatoryValue):
            # noinspection PyStatementEffect
            conf.mandatory_missing

        assert type(conf._get_node("null_default")) == AnyNode
        assert type(conf._get_node("int_default")) == AnyNode
        assert type(conf._get_node("float_default")) == AnyNode
        assert type(conf._get_node("str_default")) == AnyNode
        assert type(conf._get_node("bool_default")) == AnyNode
        assert type(conf._get_node("mandatory_missing")) == AnyNode

        assert conf.int_default == expected.int_default
        with raises(ValidationError):
            conf.typed_int_default = "foo"

        values = [10, True, False, None, 1.0, -1.0, "10", float("inf")]
        for val in values:
            conf.null_default = val
            conf.int_default = val
            conf.float_default = val
            conf.str_default = val
            conf.bool_default = val

            assert conf.null_default == val
            assert conf.int_default == val
            assert conf.float_default == val
            assert conf.str_default == val
            assert conf.bool_default == val

    def test_interpolation(self, module: Any) -> Any:
        input_ = module.Interpolation()
        conf = OmegaConf.structured(input_)
        assert conf.x == input_.x
        assert conf.z1 == conf.x
        assert conf.z2 == f"{conf.x}_{conf.y}"
        assert type(conf.x) == int
        assert type(conf.y) == int
        assert type(conf.z1) == int
        assert type(conf.z2) == str

    @mark.parametrize(
        "tested_type",
        [
            "BoolOptional",
            "IntegerOptional",
            "FloatOptional",
            "StringOptional",
            "ListOptional",
            "TupleOptional",
            "EnumOptional",
            "StructuredOptional",
            "DictOptional",
        ],
    )
    def test_optional(self, module: Any, tested_type: str) -> None:
        input_ = getattr(module, tested_type)
        obj = input_()
        conf = OmegaConf.structured(input_)

        # verify non-optional fields are rejecting None
        with raises(ValidationError):
            conf.not_optional = None

        assert conf.as_none is None
        assert conf.with_default == obj.with_default
        # assign None to an optional field
        conf.with_default = None
        assert conf.with_default is None

    def test_list_field(self, module: Any) -> None:
        input_ = module.WithListField
        conf = OmegaConf.structured(input_)
        with raises(ValidationError):
            conf.list[0] = "fail"

        with raises(ValidationError):
            conf.list.append("fail")

        with raises(ValidationError):
            cfg2 = OmegaConf.create({"list": ["fail"]})
            OmegaConf.merge(conf, cfg2)

    def test_dict_field(self, module: Any) -> None:
        input_ = module.WithDictField
        conf = OmegaConf.structured(input_)
        with raises(ValidationError):
            conf.dict["foo"] = "fail"

        with raises(ValidationError):
            OmegaConf.merge(conf, OmegaConf.create({"dict": {"foo": "fail"}}))

    @mark.skipif(sys.version_info < (3, 8), reason="requires Python 3.8 or newer")
    def test_typed_dict_field(self, module: Any) -> None:
        input_ = module.WithTypedDictField
        conf = OmegaConf.structured(input_(dict={"foo": 10}))
        assert conf.dict["foo"] == 10

        # typed dicts does not currently runtime type safety.
        conf = OmegaConf.merge(conf, {"dict": {"foo": "not_failed"}})
        assert conf.dict["foo"] == "not_failed"

    def test_merged_type1(self, module: Any) -> None:
        # Test that the merged type is that of the last merged config
        input_ = module.WithDictField
        conf = OmegaConf.structured(input_)
        res = OmegaConf.merge(OmegaConf.create(), conf)
        assert OmegaConf.get_type(res) == input_

    def test_merged_type2(self, module: Any) -> None:
        # Test that the merged type is that of the last merged config
        input_ = module.WithDictField
        conf = OmegaConf.structured(input_)
        res = OmegaConf.merge(conf, {"dict": {"foo": 99}})
        assert OmegaConf.get_type(res) == input_

    def test_merged_with_subclass(self, module: Any) -> None:
        # Test that the merged type is that of the last merged config
        c1 = OmegaConf.structured(module.Plugin)
        c2 = OmegaConf.structured(module.ConcretePlugin)
        res = OmegaConf.merge(c1, c2)
        assert OmegaConf.get_type(res) == module.ConcretePlugin

    def test_merge_missing_structured_on_self(self, module: Any) -> None:
        c1 = OmegaConf.structured(module.MissingStructuredConfigField)
        assert OmegaConf.is_missing(c1, "plugin")
        c2 = OmegaConf.merge(c1, module.MissingStructuredConfigField)
        assert OmegaConf.is_missing(c2, "plugin")

    def test_merge_missing_structured_config_is_missing(self, module: Any) -> None:
        c1 = OmegaConf.structured(module.MissingStructuredConfigField)
        assert OmegaConf.is_missing(c1, "plugin")

    def test_merge_missing_structured(self, module: Any) -> None:
        # Test that the merged type is that of the last merged config
        c1 = OmegaConf.create({"plugin": "???"})
        c2 = OmegaConf.merge(c1, module.MissingStructuredConfigField)
        assert OmegaConf.is_missing(c2, "plugin")

    def test_merge_none_is_none(self, module: Any) -> None:
        # Test that the merged type is that of the last merged config
        c1 = OmegaConf.structured(module.StructuredOptional)
        assert c1.with_default == module.Nested()
        c2 = OmegaConf.merge(c1, {"with_default": None})
        assert c2.with_default is None

    def test_merge_with_subclass_into_missing(self, module: Any) -> None:
        base = OmegaConf.structured(module.PluginHolder)
        assert _utils.get_type_hint(base, "missing") == module.Plugin
        assert OmegaConf.get_type(base, "missing") is None
        res = OmegaConf.merge(base, {"missing": module.Plugin})
        assert OmegaConf.get_type(res) == module.PluginHolder
        assert _utils.get_type_hint(base, "missing") == module.Plugin
        assert OmegaConf.get_type(res, "missing") == module.Plugin

    def test_merged_with_nons_subclass(self, module: Any) -> None:
        c1 = OmegaConf.structured(module.Plugin)
        c2 = OmegaConf.structured(module.FaultyPlugin)
        with raises(ValidationError):
            OmegaConf.merge(c1, c2)

    def test_merge_into_Dict(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.DictExamples)
        res = OmegaConf.merge(cfg, {"strings": {"x": "abc"}})
        assert res.strings == {"a": "foo", "b": "bar", "x": "abc"}

    def test_merge_user_list_with_wrong_key(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.UserList)
        with raises(ConfigKeyError):
            OmegaConf.merge(cfg, {"list": [{"foo": "var"}]})

    def test_merge_list_with_correct_type(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.UserList)
        user = module.User(name="John", age=21)
        res = OmegaConf.merge(cfg, {"list": [user]})
        assert res.list == [user]

    def test_merge_dict_with_wrong_type(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.UserDict)
        with raises(ValidationError):
            OmegaConf.merge(cfg, {"dict": {"foo": "var"}})

    def test_merge_dict_with_correct_type(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.UserDict)
        user = module.User(name="John", age=21)
        res = OmegaConf.merge(cfg, {"dict": {"foo": user}})
        assert res.dict == {"foo": user}

    def test_dict_field_key_type_error(self, module: Any) -> None:
        input_ = module.ErrorDictObjectKey
        with raises(KeyValidationError):
            OmegaConf.structured(input_)

    def test_dict_field_value_type_error(self, module: Any) -> None:
        input_ = module.ErrorDictUnsupportedValue
        with raises(ValidationError):
            OmegaConf.structured(input_)

    def test_list_field_value_type_error(self, module: Any) -> None:
        input_ = module.ErrorListUnsupportedValue
        with raises(ValidationError):
            OmegaConf.structured(input_)

    @mark.parametrize("example", ["ListExamples", "TupleExamples"])
    def test_list_examples(self, module: Any, example: str) -> None:
        input_ = getattr(module, example)
        conf = OmegaConf.structured(input_)

        def test_any(name: str) -> None:
            conf[name].append(True)
            conf[name].extend([Color.RED, 3.1415])
            conf[name][2] = False
            assert conf[name] == [1, "foo", False, Color.RED, 3.1415]

        # any and untyped
        test_any("any")

        # test ints
        with raises(ValidationError):
            conf.ints[0] = "foo"
        conf.ints.append(10)
        assert conf.ints == [1, 2, 10]

        # test strings
        conf.strings.append(Color.BLUE)
        assert conf.strings == ["foo", "bar", "Color.BLUE"]

        # test booleans
        with raises(ValidationError):
            conf.booleans[0] = "foo"
        conf.booleans.append(True)
        conf.booleans.append("off")
        conf.booleans.append(1)
        assert conf.booleans == [True, False, True, False, True]

        # test colors
        with raises(ValidationError):
            conf.colors[0] = "foo"
        conf.colors.append(Color.BLUE)
        conf.colors.append("RED")
        conf.colors.append("Color.GREEN")
        conf.colors.append(3)
        assert conf.colors == [
            Color.RED,
            Color.GREEN,
            Color.BLUE,
            Color.RED,
            Color.GREEN,
            Color.BLUE,
        ]

    def test_dict_examples_any(self, module: Any) -> None:
        conf = OmegaConf.structured(module.DictExamples)

        dct = conf.any
        dct.c = True
        dct.d = Color.RED
        dct.e = 3.1415
        assert dct == {"a": 1, "b": "foo", "c": True, "d": Color.RED, "e": 3.1415}

    def test_dict_examples_int(self, module: Any) -> None:
        conf = OmegaConf.structured(module.DictExamples)
        dct = conf.ints

        # test ints
        with raises(ValidationError):
            dct.a = "foo"
        dct.c = 10
        assert dct == {"a": 10, "b": 20, "c": 10}

    def test_dict_examples_strings(self, module: Any) -> None:
        conf = OmegaConf.structured(module.DictExamples)

        # test strings
        conf.strings.c = Color.BLUE
        assert conf.strings == {"a": "foo", "b": "bar", "c": "Color.BLUE"}

    def test_dict_examples_bool(self, module: Any) -> None:
        conf = OmegaConf.structured(module.DictExamples)
        dct = conf.booleans

        # test bool
        with raises(ValidationError):
            dct.a = "foo"
        dct.c = True
        dct.d = "off"
        dct.e = 1
        assert dct == {
            "a": True,
            "b": False,
            "c": True,
            "d": False,
            "e": True,
        }

    class TestDictExamples:
        @fixture
        def conf(self, module: Any) -> DictConfig:
            conf: DictConfig = OmegaConf.structured(module.DictExamples)
            return conf

        def test_dict_examples_colors(self, conf: DictConfig) -> None:
            dct = conf.colors

            # test colors
            with raises(ValidationError):
                dct.foo = "foo"
            dct.c = Color.BLUE
            dct.d = "RED"
            dct.e = "Color.GREEN"
            dct.f = 3
            assert dct == {
                "red": Color.RED,
                "green": Color.GREEN,
                "blue": Color.BLUE,
                "c": Color.BLUE,
                "d": Color.RED,
                "e": Color.GREEN,
                "f": Color.BLUE,
            }

        def test_dict_examples_str_keys(self, conf: DictConfig) -> None:
            dct = conf.any

            with raises(KeyValidationError):
                dct[123] = "bad key type"
            dct["c"] = "three"
            assert dct == {
                "a": 1,
                "b": "foo",
                "c": "three",
            }

        def test_dict_examples_int_keys(self, conf: DictConfig) -> None:
            dct = conf.int_keys

            # test int keys
            with raises(KeyValidationError):
                dct.foo_key = "foo_value"
            dct[3] = "three"
            assert dct == {
                1: "one",
                2: "two",
                3: "three",
            }

        def test_dict_examples_float_keys(self, conf: DictConfig) -> None:
            dct = conf.float_keys

            # test float keys
            with raises(KeyValidationError):
                dct.foo_key = "foo_value"
            dct[3.3] = "three"
            assert dct == {
                1.1: "one",
                2.2: "two",
                3.3: "three",
            }

        def test_dict_examples_bool_keys(self, conf: DictConfig) -> None:
            dct = conf.bool_keys

            # test bool_keys
            with raises(KeyValidationError):
                dct.foo_key = "foo_value"
            dct[True] = "new value"
            assert dct == {
                True: "new value",
                False: "F",
            }

        def test_dict_examples_enum_key(self, conf: DictConfig) -> None:
            dct = conf.enum_key

            # When an Enum is a dictionary key the name of the Enum is actually used
            # as the key
            assert dct.RED == "red"
            assert dct["GREEN"] == "green"
            assert dct[Color.GREEN] == "green"

            dct["BLUE"] = "Blue too"
            assert dct[Color.BLUE] == "Blue too"
            with raises(KeyValidationError):
                dct["error"] = "error"

    def test_dict_of_objects(self, module: Any) -> None:
        conf = OmegaConf.structured(module.DictOfObjects)
        dct = conf.users
        assert dct.joe.age == 18
        assert dct.joe.name == "Joe"

        dct.bond = module.User(name="James Bond", age=7)
        assert dct.bond.name == "James Bond"
        assert dct.bond.age == 7

        with raises(ValidationError):
            dct.fail = "fail"

    def test_dict_of_objects_missing(self, module: Any) -> None:
        conf = OmegaConf.structured(module.DictOfObjectsMissing)
        dct = conf.users

        assert OmegaConf.is_missing(dct, "moe")

        dct.miss = MISSING
        assert OmegaConf.is_missing(dct, "miss")

    def test_assign_dict_of_objects(self, module: Any) -> None:
        conf = OmegaConf.structured(module.DictOfObjects)
        conf.users = {"poe": module.User(name="Poe", age=8), "miss": MISSING}
        assert conf.users == {"poe": {"name": "Poe", "age": 8}, "miss": "???"}

    def test_list_of_objects(self, module: Any) -> None:
        conf = OmegaConf.structured(module.ListOfObjects)
        assert conf.users[0].age == 18
        assert conf.users[0].name == "Joe"

        conf.users.append(module.User(name="James Bond", age=7))
        assert conf.users[1].name == "James Bond"
        assert conf.users[1].age == 7

        with raises(ValidationError):
            conf.users.append("fail")

    def test_list_of_objects_missing(self, module: Any) -> None:
        conf = OmegaConf.structured(module.ListOfObjectsMissing)

        assert OmegaConf.is_missing(conf.users, 0)

        conf.users.append(MISSING)
        assert OmegaConf.is_missing(conf.users, 1)

    def test_assign_list_of_objects(self, module: Any) -> None:
        conf = OmegaConf.structured(module.ListOfObjects)
        conf.users = [module.User(name="Poe", age=8), MISSING]
        assert conf.users == [{"name": "Poe", "age": 8}, "???"]

    def test_promote_api(self, module: Any) -> None:
        conf = OmegaConf.create(module.AnyTypeConfig)
        conf._promote(None)
        assert conf == OmegaConf.create(module.AnyTypeConfig)
        with raises(ValueError):
            conf._promote(42)
        assert conf == OmegaConf.create(module.AnyTypeConfig)

    def test_promote_to_class(self, module: Any) -> None:

        conf = OmegaConf.create(module.AnyTypeConfig)
        assert OmegaConf.get_type(conf) == module.AnyTypeConfig

        conf._promote(module.BoolConfig)

        assert OmegaConf.get_type(conf) == module.BoolConfig
        assert conf.with_default is True
        assert conf.null_default is None
        assert OmegaConf.is_missing(conf, "mandatory_missing")

    def test_promote_to_object(self, module: Any) -> None:

        conf = OmegaConf.create(module.AnyTypeConfig)
        assert OmegaConf.get_type(conf) == module.AnyTypeConfig

        conf._promote(module.BoolConfig(with_default=False))
        assert OmegaConf.get_type(conf) == module.BoolConfig
        assert conf.with_default is False

    def test_promote_to_dataclass(self, module: Any) -> None:
        @dataclasses.dataclass
        class Foo:
            foo: pathlib.Path
            bar: str
            qub: int = 5

        x = DictConfig({"foo": "hello.txt", "bar": "hello.txt"})
        assert isinstance(x.foo, str)
        assert isinstance(x.bar, str)

        x._promote(Foo)
        assert isinstance(x.foo, pathlib.Path)
        assert isinstance(x.bar, str)
        assert x.qub == 5

    def test_set_key_with_with_dataclass(self, module: Any) -> None:
        cfg = OmegaConf.create({"foo": [1, 2]})
        cfg.foo = module.ListClass()

    def test_set_list_correct_type(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.ListClass)
        value = [1, 2, 3]
        cfg.list = value
        cfg.tuple = value
        assert cfg.list == value
        assert cfg.tuple == value

    @mark.parametrize(
        "value",
        [
            1,
            True,
            "str",
            3.1415,
            ["foo", True, 1.2],
            User(),
            param([None]),
        ],
    )
    def test_assign_wrong_type_to_list(self, module: Any, value: Any) -> None:
        cfg = OmegaConf.structured(module.ListClass)
        with raises(ValidationError):
            cfg.list = value
        with raises(ValidationError):
            cfg.tuple = value
        assert cfg == OmegaConf.structured(module.ListClass)

    @mark.parametrize(
        "value",
        [
            param(None),
            True,
            "str",
            3.1415,
            User(),
        ],
    )
    def test_insert_wrong_type_to_list(self, module: Any, value: Any) -> None:
        cfg = OmegaConf.structured(module.ListClass)
        with raises(ValidationError):
            cfg.list.insert(0, value)

    @mark.parametrize(
        "value",
        [
            1,
            True,
            "str",
            3.1415,
            ["foo", True, 1.2],
            {"foo": True},
            param({"foo": None}),
            User(age=1, name="foo"),
            {"user": User(age=1, name="foo")},
            ListConfig(content=[1, 2], ref_type=List[int], element_type=int),
        ],
    )
    def test_assign_wrong_type_to_dict(self, module: Any, value: Any) -> None:
        cfg = OmegaConf.structured(module.ConfigWithDict2)
        with raises(ValidationError):
            cfg.dict1 = value
        assert cfg == OmegaConf.structured(module.ConfigWithDict2)

    def test_recursive_dict(self, module: Any) -> None:
        rd = module.RecursiveDict
        o = rd(d={"a": rd(), "b": rd()})
        cfg = OmegaConf.structured(o)
        assert cfg == {
            "d": {
                "a": {"d": "???"},
                "b": {"d": "???"},
            }
        }

    def test_recursive_list(self, module: Any) -> None:
        rl = module.RecursiveList
        o = rl(d=[rl(), rl()])
        cfg = OmegaConf.structured(o)
        assert cfg == {"d": [{"d": "???"}, {"d": "???"}]}

    def test_create_untyped_dict(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.UntypedDict)
        assert _utils.get_type_hint(cfg, "dict") == Dict[Any, Any]
        assert _utils.get_type_hint(cfg, "opt_dict") == Optional[Dict[Any, Any]]
        assert cfg.dict == {"foo": "var"}
        assert cfg.opt_dict is None

    def test_create_untyped_list(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.UntypedList)
        assert _utils.get_type_hint(cfg, "list") == List[Any]
        assert _utils.get_type_hint(cfg, "opt_list") == Optional[List[Any]]
        assert cfg.list == [1, 2]
        assert cfg.opt_list is None

    def test_has_bad_annotation1(self, module: Any) -> None:
        with raises(ValidationError, match="Unexpected type annotation: object"):
            OmegaConf.structured(module.HasBadAnnotation1)

    def test_has_bad_annotation2(self, module: Any) -> None:
        with raises(
            ValidationError,
            match="Unexpected type annotation: <object object at 0x[a-fA-F0-9]*>",
        ):
            OmegaConf.structured(module.HasBadAnnotation2)

    @mark.parametrize(
        "input_, expected, expected_type, expected_ref_type, expected_object_type",
        [
            param(
                lambda module: module.HasBadAnnotation1,
                {"data": "???"},
                AnyNode,
                Any,
                None,
            ),
            param(
                lambda module: module.HasBadAnnotation1(123),
                {"data": 123},
                AnyNode,
                Any,
                None,
            ),
            param(
                lambda module: module.HasBadAnnotation1([1, 2, 3]),
                {"data": [1, 2, 3]},
                ListConfig,
                object,
                list,
            ),
            param(
                lambda module: module.HasBadAnnotation1({1: 2}),
                {"data": {1: 2}},
                DictConfig,
                object,
                dict,
            ),
            param(
                lambda module: module.HasBadAnnotation1(module.UserWithDefaultName),
                {"data": {"name": "bob", "age": "???"}},
                DictConfig,
                object,
                lambda module: module.UserWithDefaultName,
            ),
        ],
    )
    def test_bad_annotation_allow_objects(
        self,
        module: Any,
        input_: Any,
        expected: Any,
        expected_type: Any,
        expected_ref_type: Any,
        expected_object_type: Any,
    ) -> None:
        """
        Test how unsupported annotation types are handled when `allow_objects` is True
        """
        input_ = input_(module)
        if isinstance(expected_object_type, LambdaType):
            expected_object_type = expected_object_type(module)

        cfg = OmegaConf.structured(input_, flags={"allow_objects": True})

        assert cfg == expected
        assert isinstance(cfg._get_node("data"), expected_type)
        assert cfg._get_node("data")._metadata.ref_type is expected_ref_type
        assert cfg._get_node("data")._metadata.object_type is expected_object_type


def validate_frozen_impl(conf: DictConfig) -> None:
    with raises(ReadonlyConfigError):
        conf.x = 20

    with raises(ReadonlyConfigError):
        conf.list[0] = 10

    with raises(ReadonlyConfigError):
        conf.user.age = 20

    # merge into is rejected because it mutates a readonly object
    with raises(ReadonlyConfigError):
        conf.merge_with({"user": {"name": "iceman"}})

    # Normal merge is allowed.
    ret = OmegaConf.merge(conf, {"user": {"name": "iceman"}})
    assert ret == {"user": {"name": "iceman", "age": 10}, "x": 10, "list": [1, 2, 3]}
    with raises(ReadonlyConfigError):
        ret.user.age = 20


def test_frozen(module: Any) -> None:
    FrozenClass = module.FrozenClass

    validate_frozen_impl(OmegaConf.structured(FrozenClass))
    validate_frozen_impl(OmegaConf.structured(FrozenClass()))


def test_forward_ref(module: Any) -> None:
    C = module.HasForwardRef
    obj = C(a=C.CA(), b=C.CB(C.CA(x=33)))
    OmegaConf.create(obj)


class TestDictSubclass:
    def test_str2str(self, module: Any) -> None:
        with warns_dict_subclass_deprecated(module.DictSubclass.Str2Str):
            cfg = OmegaConf.structured(module.DictSubclass.Str2Str())
        cfg.hello = "world"
        assert cfg.hello == "world"

        with raises(KeyValidationError):
            cfg[Color.RED]

    def test_dict_subclass_data_preserved_upon_node_creation(self, module: Any) -> None:
        src = module.DictSubclass.Str2StrWithField()
        src["baz"] = "qux"
        with warns_dict_subclass_deprecated(module.DictSubclass.Str2StrWithField):
            cfg = OmegaConf.structured(src)
        assert cfg.foo == "bar"
        assert cfg.baz == "qux"

    def test_create_dict_subclass_with_bad_value_type(self, module: Any) -> None:
        src = module.DictSubclass.Str2Int()
        src["baz"] = "qux"
        with raises(ValidationError):
            with warns_dict_subclass_deprecated(module.DictSubclass.Str2Int):
                OmegaConf.structured(src)

    def test_str2str_as_sub_node(self, module: Any) -> None:
        with warns_dict_subclass_deprecated(module.DictSubclass.Str2Str):
            cfg = OmegaConf.create({"foo": module.DictSubclass.Str2Str})
        assert OmegaConf.get_type(cfg.foo) == module.DictSubclass.Str2Str
        assert _utils.get_type_hint(cfg.foo) == Any

        cfg.foo.hello = "world"
        assert cfg.foo.hello == "world"

        with raises(KeyValidationError):
            cfg.foo[Color.RED] = "fail"

        with raises(KeyValidationError):
            cfg.foo[123] = "fail"

    def test_int2str(self, module: Any) -> None:
        with warns_dict_subclass_deprecated(module.DictSubclass.Int2Str):
            cfg = OmegaConf.structured(module.DictSubclass.Int2Str())

        cfg[10] = "ten"  # okay
        assert cfg[10] == "ten"

        with raises(KeyValidationError):
            cfg[10.0] = "float"  # fail

        with raises(KeyValidationError):
            cfg["10"] = "string"  # fail

        with raises(KeyValidationError):
            cfg.hello = "fail"

        with raises(KeyValidationError):
            cfg[Color.RED] = "fail"

    def test_int2str_as_sub_node(self, module: Any) -> None:
        with warns_dict_subclass_deprecated(module.DictSubclass.Int2Str):
            cfg = OmegaConf.create({"foo": module.DictSubclass.Int2Str})
        assert OmegaConf.get_type(cfg.foo) == module.DictSubclass.Int2Str
        assert _utils.get_type_hint(cfg.foo) == Any

        cfg.foo[10] = "ten"
        assert cfg.foo[10] == "ten"

        with raises(KeyValidationError):
            cfg.foo[10.0] = "float"  # fail

        with raises(KeyValidationError):
            cfg.foo["10"] = "string"  # fail

        with raises(KeyValidationError):
            cfg.foo.hello = "fail"

        with raises(KeyValidationError):
            cfg.foo[Color.RED] = "fail"

    def test_color2str(self, module: Any) -> None:
        with warns_dict_subclass_deprecated(module.DictSubclass.Color2Str):
            cfg = OmegaConf.structured(module.DictSubclass.Color2Str())
        cfg[Color.RED] = "red"

        with raises(KeyValidationError):
            cfg.greeen = "nope"

        with raises(KeyValidationError):
            cfg[123] = "nope"

    def test_color2color(self, module: Any) -> None:
        with warns_dict_subclass_deprecated(module.DictSubclass.Color2Color):
            cfg = OmegaConf.structured(module.DictSubclass.Color2Color())

        # add key
        cfg[Color.RED] = "GREEN"
        assert cfg[Color.RED] == Color.GREEN

        # replace key
        cfg[Color.RED] = "RED"
        assert cfg[Color.RED] == Color.RED

        cfg[Color.BLUE] = Color.BLUE
        assert cfg[Color.BLUE] == Color.BLUE

        cfg.RED = Color.RED
        assert cfg.RED == Color.RED

        with raises(ValidationError):
            # bad value
            cfg[Color.GREEN] = 10

        with raises(ValidationError):
            # bad value
            cfg[Color.GREEN] = "this string is not a color"

        with raises(KeyValidationError):
            # bad key
            cfg.greeen = "nope"

    def test_str2user(self, module: Any) -> None:
        with warns_dict_subclass_deprecated(module.DictSubclass.Str2User):
            cfg = OmegaConf.structured(module.DictSubclass.Str2User())

        cfg.bond = module.User(name="James Bond", age=7)
        assert cfg.bond.name == "James Bond"
        assert cfg.bond.age == 7

        with raises(ValidationError):
            # bad value
            cfg.hello = "world"

        with raises(KeyValidationError):
            # bad key
            cfg[Color.BLUE] = "nope"

    def test_str2str_with_field(self, module: Any) -> None:
        with warns_dict_subclass_deprecated(module.DictSubclass.Str2StrWithField):
            cfg = OmegaConf.structured(module.DictSubclass.Str2StrWithField())
        assert cfg.foo == "bar"
        cfg.hello = "world"
        assert cfg.hello == "world"

        with raises(KeyValidationError):
            cfg[Color.RED] = "fail"

    class TestErrors:
        def test_usr2str(self, module: Any) -> None:
            with raises(KeyValidationError):
                OmegaConf.structured(module.DictSubclass.Error.User2Str())

        def test_str2int_with_field_of_different_type(self, module: Any) -> None:
            with warns_dict_subclass_deprecated(
                module.DictSubclass.Str2IntWithStrField
            ):
                cfg = OmegaConf.structured(module.DictSubclass.Str2IntWithStrField())
            with raises(ValidationError):
                cfg.foo = "str"


class TestConfigs2:
    def test_construct_from_another_retain_node_types(self, module: Any) -> None:
        cfg1 = OmegaConf.create(module.User(name="James Bond", age=7))
        with raises(ValidationError):
            cfg1.age = "not a number"

        cfg2 = OmegaConf.create(cfg1)
        with raises(ValidationError):
            cfg2.age = "not a number"

    def test_nested_with_any_var_type(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.NestedWithAny)
        assert cfg == {
            "var": {
                "with_default": 10,
                "null_default": None,
                "mandatory_missing": "???",
                "interpolation": "${value_at_root}",
            }
        }

    def test_noop_merge_into_frozen(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.ContainsFrozen)
        ret = OmegaConf.merge(cfg, {"x": 20, "frozen": {}})
        assert ret == {
            "x": 20,
            "frozen": {"user": {"name": "Bart", "age": 10}, "x": 10, "list": [1, 2, 3]},
        }

    def test_merge_into_none_list(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.ListOptional)
        assert OmegaConf.merge(cfg, {"as_none": [4, 5, 6]}) == {
            "with_default": [1, 2, 3],
            "as_none": [4, 5, 6],
            "not_optional": [1, 2, 3],
        }

        assert OmegaConf.merge(cfg, cfg) == cfg

    def test_merge_into_none_dict(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.DictOptional)
        assert OmegaConf.merge(cfg, {"as_none": {"x": 100}}) == {
            "with_default": {"a": 10},
            "as_none": {"x": 100},
            "not_optional": {"a": 10},
        }

        assert OmegaConf.merge(cfg, cfg) == cfg

    def test_set_value_after_merge_into_none_dict(self, module: Any) -> None:
        cfg = OmegaConf.structured(module.DictOptional)
        merged = OmegaConf.merge(cfg, {"as_none": {"x": 100}})
        with raises(ValidationError):
            merged.as_none.x = "abc"
        with raises(ValidationError):
            merged.as_none.y = "abc"

    @mark.parametrize(
        "update_value,expected",
        [
            param([], {"list": []}, id="empty"),
            param(
                [{"name": "Bond"}],
                {"list": [{"name": "Bond", "age": "???"}]},
                id="partial",
            ),
            param(
                [{"name": "Bond", "age": 7}],
                {"list": [{"name": "Bond", "age": 7}]},
                id="complete",
            ),
            param(
                [{"age": "double o seven"}],
                raises(ValidationError),
                id="complete",
            ),
        ],
    )
    def test_update_userlist(
        self, module: Any, update_value: Any, expected: Any
    ) -> None:
        cfg = OmegaConf.structured(module.UserList)
        if isinstance(expected, dict):
            OmegaConf.update(cfg, "list", update_value)
            assert cfg == expected
        else:
            with raises(ValidationError):
                OmegaConf.update(cfg, "list", update_value)

    def test_merge_missing_list_promotes_target_type(self, module: Any) -> None:
        c1 = OmegaConf.create({"missing": []})
        c2 = OmegaConf.structured(module.ConfigWithList)
        c3 = OmegaConf.merge(c1, c2)
        with raises(ValidationError):
            c3.missing.append("xx")

    def test_ignore_metadata_class_with_required_args(self, module: Any) -> None:
        cfg = OmegaConf.create(module.HasIgnoreMetadataRequired)
        assert cfg == {"no_ignore": "???"}

    def test_ignore_metadata_instance_with_required_args(self, module: Any) -> None:
        cfg = OmegaConf.create(module.HasIgnoreMetadataRequired(3, 4))
        assert cfg == {"no_ignore": 4}

    def test_ignore_metadata_class_with_default_args(self, module: Any) -> None:
        cfg = OmegaConf.create(module.HasIgnoreMetadataWithDefault)
        assert cfg == {"no_ignore": 2}

    def test_ignore_metadata_instance_with_default_args(self, module: Any) -> None:
        cfg = OmegaConf.create(module.HasIgnoreMetadataWithDefault(3, 4))
        assert cfg == {"no_ignore": 4}


class TestStructredConfigInheritance:
    def test_leaf_node_inheritance(self, module: Any) -> None:
        parent = OmegaConf.structured(module.StructuredSubclass.ParentInts)
        child = OmegaConf.structured(module.StructuredSubclass.ChildInts)

        assert OmegaConf.is_missing(parent, "int1")
        assert OmegaConf.is_missing(child, "int1")

        assert OmegaConf.is_missing(parent, "int2")
        assert child.int2 == 5

        assert OmegaConf.is_missing(parent, "int3")
        assert child.int3 == 10

        assert OmegaConf.is_missing(parent, "int4")
        assert child.int4 == 15

    def test_container_inheritance(self, module: Any) -> None:
        parent = OmegaConf.structured(module.StructuredSubclass.ParentContainers)
        child = OmegaConf.structured(module.StructuredSubclass.ChildContainers)

        assert OmegaConf.is_missing(parent, "list1")
        assert child.list1 == [1, 2, 3]

        assert parent.list2 == [5, 6]
        assert child.list2 == [5, 6]

        assert OmegaConf.is_missing(parent, "dict")
        assert child.dict == {"a": 5, "b": 6}

    @mark.parametrize(
        "create_fn",
        [
            param(lambda cls: OmegaConf.structured(cls), id="create_from_class"),
            param(lambda cls: OmegaConf.structured(cls()), id="create_from_instance"),
        ],
    )
    def test_subclass_using_default_factory(
        self, module: Any, create_fn: Callable[[Any], DictConfig]
    ) -> None:
        """
        When a structured config field has a default and a subclass defines a
        default_factory for the same field, ensure that the DictConfig created
        from the subclass uses the subclass' default_factory (not the parent
        class' default).
        """
        cfg = create_fn(module.StructuredSubclass.ChildWithDefaultFactory)
        assert cfg.no_default_to_list == ["hi"]
        assert cfg.int_to_list == ["hi"]


class TestNestedContainers:
    @mark.parametrize(
        "class_name",
        [
            "ListOfLists",
            "DictOfDicts",
            "ListsAndDicts",
            "WithDefault",
        ],
    )
    def test_instantiation(self, module: Any, class_name: str) -> None:
        cls = getattr(module.NestedContainers, class_name)
        OmegaConf.structured(cls)

    @mark.parametrize(
        "class_name, keys, expected_optional, expected_ref_type",
        [
            param("ListOfLists", "lls", False, List[List[str]], id="lls"),
            param(
                "ListOfLists",
                "llx",
                False,
                lambda module: List[List[module.User]],  # type: ignore
                id="llx",
            ),
            param("ListOfLists", "llla", False, List[List[List[Any]]], id="llla"),
            param(
                "ListOfLists",
                "lloli",
                False,
                List[List[Optional[List[int]]]],
                id="lloli",
            ),
            param(
                "ListOfLists",
                "lloli",
                False,
                List[List[Optional[List[int]]]],
                id="lloli",
            ),
            param(
                "ListOfLists", "lls_default", False, List[List[str]], id="lls_default"
            ),
            param(
                "ListOfLists", "lls_default", False, List[List[str]], id="lls_default"
            ),
            param(
                "ListOfLists", ["lls_default", 0], False, List[str], id="lls_default-0"
            ),
            param(
                "ListOfLists", ["lls_default", 1], False, List[str], id="lls_default-1"
            ),
            param(
                "ListOfLists", ["lls_default", 2], False, List[str], id="lls_default-2"
            ),
            param(
                "ListOfLists",
                "lolx_default",
                False,
                lambda module: List[Optional[List[module.User]]],  # type: ignore
                id="lolx_default",
            ),
            param(
                "ListOfLists",
                ["lolx_default", 0],
                True,
                lambda module: List[module.User],  # type: ignore
                id="lolx_default-0",
            ),
            param(
                "ListOfLists",
                ["lolx_default", 1],
                True,
                lambda module: List[module.User],  # type: ignore
                id="lolx_default-1",
            ),
            param(
                "ListOfLists",
                ["lolx_default", 2],
                True,
                lambda module: List[module.User],  # type: ignore
                id="lolx_default-2",
            ),
            param("DictOfDicts", "dsdsi", False, Dict[str, Dict[str, int]], id="dsdsi"),
            param(
                "DictOfDicts",
                ["odsdsi_default", "dsi1"],
                False,
                Dict[str, int],
                id="odsdsi_default-dsi1",
            ),
            param(
                "DictOfDicts",
                ["odsdsi_default", "dsi2"],
                False,
                Dict[str, int],
                id="odsdsi_default-dsi2",
            ),
            param(
                "DictOfDicts",
                ["odsdsi_default", "dsi3"],
                False,
                Dict[str, int],
                id="odsdsi_default-dsi3",
            ),
            param(
                "DictOfDicts",
                ["dsdsx_default", "dsx1"],
                False,
                lambda module: Dict[str, module.User],  # type: ignore
                id="dsdsx_default-dsx1",
            ),
            param(
                "DictOfDicts",
                ["dsdsx_default", "dsx2"],
                False,
                lambda module: Dict[str, module.User],  # type: ignore
                id="dsdsx_default-dsx2",
            ),
            param(
                "DictOfDicts",
                ["dsdsx_default", "dsx2", "s1"],
                False,
                lambda module: module.User,
                id="dsdsx_default-dsx2-s1",
            ),
            param(
                "DictOfDicts",
                ["dsdsx_default", "dsx2", "s2"],
                False,
                lambda module: module.User,
                id="dsdsx_default-dsx2-s2",
            ),
            param(
                "DictOfDicts",
                ["dsdsx_default", "dsx2", "s3"],
                False,
                lambda module: module.User,
                id="dsdsx_default-dsx2-s3",
            ),
            param(
                "DictOfDicts",
                ["dsdsx_default", "dsx3"],
                False,
                lambda module: Dict[str, module.User],  # type: ignore
                id="dsdsx_default-dsx3",
            ),
            param(
                "ListsAndDicts", "lldsi", False, List[List[Dict[str, int]]], id="lldsi"
            ),
            param(
                "ListsAndDicts",
                "oldfox",
                True,
                lambda module: List[Dict[float, Optional[module.User]]],  # type: ignore
                id="oldfox",
            ),
            param(
                "ListsAndDicts",
                "oldfox",
                True,
                lambda module: List[Dict[float, Optional[module.User]]],  # type: ignore
                id="oldfox",
            ),
            param(
                "ListsAndDicts",
                ["dedsle_default", Color.RED],
                False,
                Dict[str, List[Enum1]],
                id="dedsle_default-RED",
            ),
            param(
                "WithDefault",
                "dsolx_default",
                False,
                lambda module: Dict[str, Optional[List[module.User]]],  # type: ignore
                id="dsolx_default",
            ),
            param(
                "WithDefault",
                ["dsolx_default", "lx"],
                True,
                lambda module: List[module.User],  # type: ignore
                id="dsolx_default-lx",
            ),
            param(
                "WithDefault",
                ["dsolx_default", "lx", 0],
                False,
                lambda module: module.User,
                id="dsolx_default-lx-0",
            ),
        ],
    )
    def test_metadata(
        self,
        module: Any,
        class_name: str,
        keys: Any,
        expected_optional: bool,
        expected_ref_type: Any,
    ) -> None:
        cls = getattr(module.NestedContainers, class_name)
        node = OmegaConf.structured(cls)

        if not isinstance(keys, list):
            keys = [keys]
        for key in keys:
            node = node._get_node(key)

        if inspect.isfunction(expected_ref_type):
            expected_ref_type = expected_ref_type(module)

        assert node._metadata.optional == expected_optional
        assert node._metadata.ref_type == expected_ref_type

        if _utils.is_dict_annotation(expected_ref_type):
            expected_key_type, expected_element_type = _utils.get_dict_key_value_types(
                expected_ref_type
            )
            assert node._metadata.key_type == expected_key_type
            assert node._metadata.element_type == expected_element_type

        if _utils.is_list_annotation(expected_ref_type):
            expected_element_type = _utils.get_list_element_type(expected_ref_type)
            assert node._metadata.element_type == expected_element_type

    @mark.parametrize(
        "class_name, key, value",
        [
            param("ListOfLists", "lls", [["a", "b"], ["c"]], id="lls"),
            param("ListOfLists", "lls", [], id="lls-empty"),
            param("ListOfLists", "lls", [[]], id="lls-list-of-empty"),
            param(
                "ListOfLists",
                "lls_default",
                [["a", "b"], ["c"]],
                id="lls_default",
            ),
            param(
                "ListOfLists",
                "llx",
                lambda module: [[module.User("Bond", 7)]],
                id="llx",
            ),
            param(
                "ListOfLists",
                "lolx_default",
                lambda module: [[module.User("Bond", 7)]],
                id="lolx_default",
            ),
            param("DictOfDicts", "dsdsi", {"abc": {"xyz": 123}}, id="dsdsi"),
            param("DictOfDicts", "dsdbi", {"abc": {True: 456}}, id="dsdbi"),
            param(
                "DictOfDicts",
                "dsdsx",
                lambda module: {"abc": {"xyz": module.User("Bond", 7)}},
                id="dsdsx",
            ),
        ],
    )
    def test_legal_assignment(
        self, module: Any, class_name: str, key: str, value: Any
    ) -> None:
        cls = getattr(module.NestedContainers, class_name)
        cfg = OmegaConf.structured(cls)
        if inspect.isfunction(value):
            value = value(module)

        cfg[key] = value

        assert cfg[key] == value

    @mark.parametrize(
        "class_name, key, value, expected",
        [
            param(
                "ListOfLists",
                "lls",
                [["123", 456]],
                [["123", "456"]],
                id="lls-conversion-from-int",
            ),
            param("ListOfLists", "lls", [["123", 456]], [["123", "456"]], id="lls"),
            param("ListOfLists", "llla", [[["123", 456]]], [[["123", 456]]], id="llla"),
            param("ListOfLists", "lloli", [[["123", 456]]], [[[123, 456]]], id="lloli"),
            param(
                "DictOfDicts",
                "dsdbi",
                {"abc": {True: "456"}},
                {"abc": {True: 456}},
                id="dsdbi",
            ),
        ],
    )
    def test_assignment_conversion(
        self, module: Any, class_name: str, key: str, value: Any, expected: Any
    ) -> None:
        cls = getattr(module.NestedContainers, class_name)
        cfg = OmegaConf.structured(cls)
        if inspect.isfunction(value):
            value = value(module)

        cfg[key] = value

        assert cfg[key] == expected

    @mark.parametrize(
        "class_name, key, value, err_type",
        [
            param(
                "ListOfLists",
                "lloli",
                [[["abc"]]],
                ValidationError,
                id="assign-llls-to-lloli",
            ),
            param(
                "ListOfLists",
                "llx",
                [[{"name": "Bond", "age": 7}]],
                ValidationError,
                id="assign-lld-to-llx",
            ),
            param(
                "DictOfDicts",
                "dsdbi",
                {123: {True: 456}},
                KeyValidationError,
                id="assign-didbi-to-dsdbi",
            ),
            param(
                "DictOfDicts",
                "dsdbi",
                {"abc": {"True": 456}},
                KeyValidationError,
                id="assign-dsdsi-to-dsdbi",
            ),
        ],
    )
    def test_illegal_assignment(
        self, module: Any, class_name: str, key: str, value: Any, err_type: Any
    ) -> None:
        cls = getattr(module.NestedContainers, class_name)
        cfg = OmegaConf.structured(cls)

        with raises(err_type):
            cfg[key] = value

    @mark.parametrize(
        "class_name, keys, expected",
        [
            param("ListOfLists", ["lls"], MISSING, id="lls-missing"),
            param("ListOfLists", ["lls_default", 0], [], id="lls_default-empty-list"),
            param("ListOfLists", ["lls_default", 1, 0], "abc", id="lls_default-str"),
            param(
                "ListOfLists",
                ["lls_default", 1, 2],
                "123",
                id="lls_default-int-converted",
            ),
            param(
                "ListOfLists",
                ["lls_default", 1, 3],
                MISSING,
                id="lls_default-missing-nested",
            ),
            param("ListOfLists", ["lls_default", 2], MISSING, id="lls_default-missing"),
            param("DictOfDicts", ["dsdsi"], MISSING, id="dsdsi-missing"),
            param(
                "DictOfDicts", ["dsdsx_default", "dsx1"], {}, id="dsdsx_default-empty"
            ),
            param(
                "DictOfDicts",
                ["dsdsx_default", "dsx2", "s1"],
                {"name": "???", "age": "???"},
                id="dsdsx_default-user-missing-data",
            ),
            param(
                "DictOfDicts",
                ["dsdsx_default", "dsx2", "s2"],
                {"name": "Bond", "age": 7},
                id="dsdsx_default-user",
            ),
            param(
                "DictOfDicts",
                ["dsdsx_default", "dsx2", "s3"],
                MISSING,
                id="dsdsx_default-missing-user",
            ),
            param(
                "DictOfDicts",
                ["odsdsi_default", "dsi2", "s2"],
                123,
                id="dsdsi-str-converted-to-int",
            ),
        ],
    )
    def test_default_values(
        self, module: Any, class_name: str, keys: Any, expected: Any
    ) -> None:
        cls = getattr(module.NestedContainers, class_name)
        node = OmegaConf.structured(cls)

        if not isinstance(keys, list):
            keys = [keys]
        for key in keys:
            node = node._get_node(key)

        if expected is MISSING:
            assert node._is_missing()
        else:
            assert node == expected

    @mark.parametrize(
        "class_name, keys, value, is_legal",
        [
            param("WithDefault", "dsolx_default", None, False, id="dsolx=none-illegal"),
            param(
                "WithDefault", ["dsolx_default", "lx"], None, True, id="olx=none-legal"
            ),
            param(
                "WithDefault", "dsolx_default", {"s": None}, True, id="dsolx=dn-legal"
            ),
            param(
                "WithDefault",
                ["dsolx_default", "lx", 0],
                None,
                False,
                id="x=none-illegal",
            ),
            param("DictOfDicts", "odsdsi_default", None, True, id="odsdsi=none-legal"),
            param("DictOfDicts", "dsdsx", None, False, id="dsdsx=none-illegal"),
            param(
                "DictOfDicts",
                ["odsdsi_default", "dsi1"],
                None,
                False,
                id="dsi=none-illegal",
            ),
            param("DictOfDicts", "dsdsx", {"s": None}, False, id="dsdsx=dsn-illegal"),
            param("ListOfLists", "lloli", None, False, id="lloli=n-illegal"),
            param("ListOfLists", "lloli", [None], False, id="lloli=ln-illegal"),
            param("ListOfLists", "lloli", [[None]], True, id="lloli=lln-legal"),
            param("ListOfLists", "lloli", [[[None]]], False, id="lloli=llln-illegal"),
            param("ListOfLists", ["lolx_default"], None, False, id="lolx=n-illegal"),
            param("ListOfLists", ["lolx_default", 1], None, True, id="olx=n-legal"),
            param(
                "ListOfLists", ["lolx_default", 1, 0], None, False, id="lx=n-illegal"
            ),
        ],
    )
    def test_assign_none(
        self, module: Any, class_name: str, keys: Any, value: Any, is_legal: bool
    ) -> None:
        cls = getattr(module.NestedContainers, class_name)
        node = OmegaConf.structured(cls)

        if not isinstance(keys, list):
            keys = [keys]
        for key in keys[:-1]:
            node = node[key]
        last_key = keys[-1]

        if is_legal:
            node[last_key] = value
            assert node[last_key] == value
        else:
            with raises(ValidationError):
                node[last_key] = value


class TestUnionsOfPrimitiveTypes:
    @mark.parametrize(
        "class_name, key, expected_type_hint, expected_val",
        [
            param("Simple", "uis", Union[int, str], MISSING, id="simple-uis"),
            param("Simple", "ubc", Union[bool, Color], MISSING, id="simple-ubc"),
            param("Simple", "uxf", Union[bytes, float], MISSING, id="simple-uxf"),
            param(
                "Simple", "ouis", Optional[Union[int, str]], MISSING, id="simple-ouis"
            ),
            param("Simple", "uisn", Union[int, str, None], MISSING, id="simple-uisn"),
            param(
                "Simple", "uisN", Union[int, str, type(None)], MISSING, id="simple-uisN"
            ),
            param("WithDefaults", "uis", Union[int, str], "abc", id="defaults-uis"),
            param("WithDefaults", "ubc1", Union[bool, Color], True, id="defaults-ubc1"),
            param(
                "WithDefaults",
                "ubc2",
                Union[bool, Color],
                Color.RED,
                id="defaults-ubc2",
            ),
            param("WithDefaults", "uxf", Union[bytes, float], 1.2, id="defaults-uxf"),
            param(
                "WithDefaults",
                "ouis",
                Optional[Union[int, str]],
                None,
                id="defaults-ouis",
            ),
            param(
                "WithDefaults", "uisn", Union[int, str, None], 123, id="defaults-uisn"
            ),
            param(
                "WithDefaults",
                "uisN",
                Union[int, str, type(None)],
                "abc",
                id="defaults-uisN",
            ),
            param(
                "WithExplicitMissing",
                "uis_missing",
                Union[int, str],
                MISSING,
                id="uis_missing",
            ),
            param(
                "ContainersOfUnions",
                "lubc",
                List[Union[bool, Color]],
                MISSING,
                id="lubc",
            ),
            param(
                "ContainersOfUnions",
                "dsubf",
                Dict[str, Union[bool, float]],
                MISSING,
                id="dsubf",
            ),
            param(
                "ContainersOfUnions",
                "lubc_with_default",
                List[Union[bool, Color]],
                [True, Color.RED],
                id="lubc_with_default",
            ),
            param(
                "ContainersOfUnions",
                "dsubf_with_default",
                Dict[str, Union[bool, float]],
                {"abc": True, "xyz": 1.2},
                id="dsubf_with_default",
            ),
            param(
                "InterpolationFromUnion",
                "ubi_with_default",
                Union[bool, int],
                "${an_int}",
                id="iterp-from-union",
            ),
            param(
                "InterpolationFromUnion",
                "ubi_with_default",
                Union[bool, int],
                123,
                id="iterp-from-union-resolved",
            ),
            param(
                "InterpolationToUnion",
                "a_float",
                float,
                10.1,
                id="iterp-to-union-resolved",
            ),
        ],
    )
    def test_union_instantiation(
        self,
        module: Any,
        class_name: str,
        key: str,
        expected_type_hint: Any,
        expected_val: Any,
    ) -> None:
        class_ = getattr(module.UnionsOfPrimitveTypes, class_name)
        cfg = OmegaConf.structured(class_)

        assert _utils.get_type_hint(cfg, key) == expected_type_hint

        vk = _utils.get_value_kind(expected_val)

        if vk is _utils.ValueKind.VALUE:
            assert cfg[key] == expected_val
            if _utils.is_primitive_type_annotation(type(expected_val)):
                assert type(cfg[key]) == type(expected_val)
            else:
                assert isinstance(cfg[key], Container)

        elif vk is _utils.ValueKind.MANDATORY_MISSING:
            assert OmegaConf.is_missing(cfg, key)
            assert cfg._get_node(key)._value() == expected_val

        elif vk is _utils.ValueKind.INTERPOLATION:
            assert OmegaConf.is_interpolation(cfg, key)
            assert cfg._get_node(key)._value() == expected_val

    @mark.parametrize(
        "class_name, expected_err",
        [
            param(
                "WithBadDefaults1",
                re.escape(
                    "Value 'None' is incompatible with type hint 'Union[int, str]"
                ),
                id="assign-none-to-uis",
            ),
            param(
                "WithBadDefaults2",
                re.escape(
                    "Value 'abc' of type 'str' is incompatible with type hint 'Union[bool, Color]'"
                ),
                id="assign-str-to-ubc",
            ),
            param(
                "WithBadDefaults3",
                re.escape(
                    "Value 'True' of type 'bool' is incompatible with type hint 'Union[bytes, float]'"
                ),
                id="assign-bool-to-uxf",
            ),
            param(
                "WithBadDefaults4",
                re.escape(
                    "Value 'Color.RED' of type 'tests.Color' is incompatible"
                    + " with type hint 'Optional[Union["
                )
                + r"(bool, float)|(float, bool)\]\]'",
                id="assign-enum-to-oufb",
            ),
        ],
    )
    def test_union_instantiation_with_bad_defaults(
        self, module: Any, class_name: str, expected_err: str
    ) -> None:
        class_ = getattr(module.UnionsOfPrimitveTypes, class_name)
        with raises(ValidationError, match=expected_err):
            OmegaConf.structured(class_)

    @mark.parametrize(
        "class_name, key, value, expected",
        [
            param("Simple", "uis", 123, 123, id="simple-uis-int"),
            param("Simple", "uis", "123", "123", id="simple-uis-int_string"),
            param("Simple", "uis", "abc", "abc", id="simple-uis-str"),
            param("Simple", "uis", None, raises(ValidationError), id="simple-uis-none"),
            param("Simple", "uis", MISSING, MISSING, id="simple-uis-missing"),
            param("Simple", "uis", "${interp}", "${interp}", id="simple-uis-interp"),
            param("Simple", "ubc", True, True, id="simple-ubc-bool"),
            param("Simple", "ubc", Color.RED, Color.RED, id="simple-ubc-color"),
            param(
                "Simple",
                "ubc",
                "RED",
                raises(ValidationError),
                id="simple-ubc-color_str",
            ),
            param(
                "Simple",
                "ubc",
                "a_string",
                raises(ValidationError),
                id="simple-ubc-str",
            ),
            param("Simple", "ubc", None, raises(ValidationError), id="simple-ubc-none"),
            param("Simple", "ubc", MISSING, MISSING, id="simple-ubc-missing"),
            param("Simple", "ubc", "${interp}", "${interp}", id="simple-ubc-interp"),
            param("Simple", "ouis", None, None, id="simple-ouis-none"),
            param("WithDefaults", "uis", 123, 123, id="with_defaults-uis-int"),
            param(
                "WithDefaults", "uis", "123", "123", id="with_defaults-uis-int_string"
            ),
            param("WithDefaults", "uis", "abc", "abc", id="with_defaults-uis-str"),
            param(
                "WithDefaults",
                "uis",
                None,
                raises(ValidationError),
                id="with_defaults-uis-none",
            ),
            param(
                "WithDefaults", "uis", MISSING, MISSING, id="with_defaults-uis-missing"
            ),
            param(
                "WithDefaults",
                "uis",
                "${interp}",
                "${interp}",
                id="with_defaults-uis-interp",
            ),
            param("WithDefaults", "ubc1", True, True, id="with_defaults-ubc-bool"),
            param(
                "WithDefaults",
                "ubc1",
                Color.RED,
                Color.RED,
                id="with_defaults-ubc-color",
            ),
            param(
                "WithDefaults",
                "ubc1",
                "RED",
                raises(ValidationError),
                id="with_defaults-ubc-color_str",
            ),
            param(
                "WithDefaults",
                "ubc1",
                "a_string",
                raises(ValidationError),
                id="with_defaults-ubc-str",
            ),
            param(
                "WithDefaults",
                "ubc1",
                None,
                raises(ValidationError),
                id="with_defaults-ubc-none",
            ),
            param(
                "WithDefaults", "ubc1", MISSING, MISSING, id="with_defaults-ubc-missing"
            ),
            param(
                "WithDefaults",
                "ubc1",
                "${interp}",
                "${interp}",
                id="with_defaults-ubc-interp",
            ),
            param("WithDefaults", "ouis", None, None, id="with_defaults-ouis-none"),
            param("ContainersOfUnions", "lubc", MISSING, MISSING, id="lubc-missing"),
            param(
                "ContainersOfUnions",
                "lubc",
                None,
                raises(ValidationError),
                id="lubc-none",
            ),
            param(
                "ContainersOfUnions", "lubc", "${interp}", "${interp}", id="lubc-interp"
            ),
            param("ContainersOfUnions", "lubc", [], [], id="lubc-list-empty"),
            param(
                "ContainersOfUnions",
                "lubc",
                [Color.GREEN],
                [Color.GREEN],
                id="lubc-list-enum",
            ),
            param(
                "ContainersOfUnions",
                "lubc",
                ["GREEN"],
                raises(ValidationError),
                id="lubc-list-enum_str",
            ),
            param(
                "ContainersOfUnions",
                "lubc",
                ["abc"],
                raises(ValidationError),
                id="lubc-list-str",
            ),
            param(
                "ContainersOfUnions",
                "lubc",
                [None],
                raises(ValidationError),
                id="lubc-list-none",
            ),
            param(
                "ContainersOfUnions",
                "lubc",
                [MISSING],
                [MISSING],
                id="lubc-list-missing",
            ),
            param(
                "ContainersOfUnions",
                "lubc",
                ["${interp}"],
                ["${interp}"],
                id="lubc-list-interp",
            ),
            param(
                "ContainersOfUnions",
                "dsubf",
                {"bool": True, "float": 10.1},
                {"bool": True, "float": 10.1},
                id="dsubf",
            ),
            param(
                "ContainersOfUnions",
                "dsubf",
                {"float-str": "10.1"},
                raises(ValidationError),
                id="dsubf-float-str",
            ),
            param(
                "ContainersOfUnions",
                "dsubf",
                {"str": "abc"},
                raises(ValidationError),
                id="dsubf-dict-str",
            ),
            param(
                "ContainersOfUnions",
                "dsubf",
                {"none": None},
                raises(ValidationError),
                id="dsubf-dict-none",
            ),
            param(
                "ContainersOfUnions",
                "dsubf",
                {"missing": MISSING},
                {"missing": MISSING},
                id="dsubf-dict-missing",
            ),
            param(
                "ContainersOfUnions",
                "dsubf",
                {"interp": "${interp}"},
                {"interp": "${interp}"},
                id="dsubf-dict-interp",
            ),
            param(
                "ContainersOfUnions",
                "dsoubf",
                {"none": None},
                {"none": None},
                id="dsoubf-dict-none",
            ),
        ],
    )
    def test_assignment_to_union(
        self, module: Any, class_name: str, key: str, value: Any, expected: Any
    ) -> None:
        class_ = getattr(module.UnionsOfPrimitveTypes, class_name)
        cfg = OmegaConf.structured(class_)

        if hasattr(expected, '__enter__'):
            with expected:
                cfg[key] = value

        else:
            cfg[key] = value

            vk = _utils.get_value_kind(expected)

            if vk is _utils.ValueKind.VALUE:
                assert cfg[key] == expected

            elif vk is _utils.ValueKind.MANDATORY_MISSING:
                assert OmegaConf.is_missing(cfg, key)
                assert _utils._get_value(cfg._get_node(key)) == expected

            elif vk is _utils.ValueKind.INTERPOLATION:
                assert OmegaConf.is_interpolation(cfg, key)
                assert _utils._get_value(cfg._get_node(key)) == expected

    @mark.parametrize(
        "key, value, expected",
        [
            param("ubi", "${an_int}", 123, id="interp-to-int"),
            param(
                "ubi",
                "${none}",
                raises(ValidationError),
                id="interp-to-none-err",
                marks=mark.xfail(reason="interpolations from unions are not validated"),
            ),
            param(
                "ubi",
                "${a_string}",
                raises(ValidationError),
                id="interp-to-str-err",
                marks=mark.xfail(reason="interpolations from unions are not validated"),
            ),
            param(
                "ubi",
                "${missing}",
                raises(InterpolationToMissingValueError),
                id="interp-to-missing",
            ),
            param("oubi", "${none}", None, id="interp-to-none"),
        ],
    )
    @mark.parametrize("overwrite_default", [True, False])
    def test_interpolation_from_union(
        self, module: Any, key: str, overwrite_default: bool, value: Any, expected: Any
    ) -> None:
        class_ = module.UnionsOfPrimitveTypes.InterpolationFromUnion
        cfg = OmegaConf.structured(class_)

        if overwrite_default:
            key += "_with_default"

        cfg[key] = value

        assert _utils._get_value(cfg._get_node(key)) == value

        if hasattr(expected, '__enter__'):
            with expected:
                cfg[key]
        else:
            assert cfg[key] == expected

    def test_resolve_union_interpolation(self, module: Any) -> None:
        class_ = module.UnionsOfPrimitveTypes.InterpolationFromUnion
        cfg = OmegaConf.structured(class_)
        assert OmegaConf.is_interpolation(cfg, "ubi_with_default")
        assert OmegaConf.is_interpolation(cfg, "oubi_with_default")
        OmegaConf.resolve(cfg)
        assert not OmegaConf.is_interpolation(cfg, "ubi_with_default")
        assert not OmegaConf.is_interpolation(cfg, "oubi_with_default")

    def test_resolve_union_interpolation_error(self, module: Any) -> None:
        class_ = module.UnionsOfPrimitveTypes.BadInterpolationFromUnion
        cfg = OmegaConf.structured(class_)
        assert OmegaConf.is_interpolation(cfg, "ubi")
        with raises(ValidationError):
            OmegaConf.resolve(cfg)

    @mark.parametrize(
        "key, expected",
        [
            param("a_float", 10.1, id="interp-to-float"),
            param("bad_int_interp", raises(ValidationError), id="bad-int-interp"),
        ],
    )
    def test_interpolation_to_union(self, module: Any, key: str, expected: Any) -> None:
        class_ = module.UnionsOfPrimitveTypes.InterpolationToUnion
        cfg = OmegaConf.structured(class_)

        if hasattr(expected, '__enter__'):
            with expected:
                cfg[key]
        else:
            assert cfg[key] == expected

    @mark.skipif(sys.version_info < (3, 10), reason="requires Python 3.10 or newer")
    def test_support_pep_604(self, module: Any) -> None:
        class_ = module.UnionsOfPrimitveTypes.SupportPEP604
        cfg = OmegaConf.structured(class_)
        assert _utils.get_type_hint(cfg, "uis") == Union[int, str]
        assert _utils.get_type_hint(cfg, "ouis") == Optional[Union[int, str]]
        assert _utils.get_type_hint(cfg, "uisn") == Optional[Union[int, str]]
        assert _utils.get_type_hint(cfg, "uis_with_default") == Union[int, str]
        assert cfg.uisn is None
        assert cfg.uis_with_default == 123

    @mark.skipif(sys.version_info < (3, 9), reason="requires Python 3.9 or newer")
    def test_support_pep_585(self, module: Any) -> None:
        class_ = module.SupportPEP585
        cfg = OmegaConf.structured(class_)
        assert _utils.get_type_hint(cfg, "dict_") == Dict[int, str]
        assert _utils.get_type_hint(cfg, "list_") == List[int]
        assert _utils.get_type_hint(cfg, "tuple_") == Tuple[int]
        assert _utils.get_type_hint(cfg, "dict_no_subscript") == Dict[Any, Any]
        assert _utils.get_type_hint(cfg, "list_no_subscript") == List[Any]
        assert _utils.get_type_hint(cfg, "tuple_no_subscript") == Tuple[Any, ...]

    def test_assign_path_to_string_typed_field(self, module: Any) -> None:
        cfg = OmegaConf.create(module.StringConfig)
        cfg.null_default = Path("hello.txt")
        assert isinstance(cfg.null_default, str)
        assert cfg.null_default == "hello.txt"
