# -*- coding: utf-8 -*-
import re
from pathlib import Path
from textwrap import dedent
from typing import Any, Callable, List, MutableSequence, Optional, Union

from pytest import mark, param, raises

from omegaconf import MISSING, AnyNode, DictConfig, ListConfig, OmegaConf, flag_override
from omegaconf._utils import _ensure_container, nullcontext
from omegaconf.base import Node
from omegaconf.errors import (
    ConfigTypeError,
    InterpolationKeyError,
    InterpolationToMissingValueError,
    KeyValidationError,
    MissingMandatoryValue,
    OmegaConfBaseException,
    UnsupportedValueType,
    ValidationError,
)
from omegaconf.nodes import IntegerNode, StringNode
from tests import Color, IllegalType, User


def test_list_value() -> None:
    c = OmegaConf.create("a: [1,2]")
    assert {"a": [1, 2]} == c


def test_list_of_dicts() -> None:
    v = [dict(key1="value1"), dict(key2="value2")]
    c = OmegaConf.create(v)
    assert c[0].key1 == "value1"
    assert c[1].key2 == "value2"


@mark.parametrize("default", [None, 0, "default"])
@mark.parametrize(
    ("cfg", "key"),
    [
        (["???"], 0),
        ([DictConfig(content="???")], 0),
        ([ListConfig(content="???")], 0),
    ],
)
def test_list_get_return_default(cfg: List[Any], key: int, default: Any) -> None:
    c = OmegaConf.create(cfg)
    val = c.get(key, default_value=default)
    assert val is default


@mark.parametrize("default", [None, 0, "default"])
@mark.parametrize(
    ("cfg", "key", "expected"),
    [
        (["found"], 0, "found"),
        ([None], 0, None),
        ([DictConfig(content=None)], 0, None),
        ([ListConfig(content=None)], 0, None),
    ],
)
def test_list_get_do_not_return_default(
    cfg: List[Any], key: int, expected: Any, default: Any
) -> None:
    c = OmegaConf.create(cfg)
    val = c.get(key, default_value=default)
    assert val == expected


@mark.parametrize(
    "input_, expected, expected_no_resolve, list_key",
    [
        param([1, 2], [1, 2], [1, 2], None, id="simple"),
        param(["${1}", 2], [2, 2], ["${1}", 2], None, id="interpolation"),
        param(
            [ListConfig(None), ListConfig("${.2}"), [1, 2]],
            [None, ListConfig([1, 2]), ListConfig([1, 2])],
            [None, ListConfig("${.2}"), ListConfig([1, 2])],
            None,
            id="iter_over_lists",
        ),
        param(
            [DictConfig(None), DictConfig("${.2}"), {"a": 10}],
            [None, DictConfig({"a": 10}), DictConfig({"a": 10})],
            [None, DictConfig("${.2}"), DictConfig({"a": 10})],
            None,
            id="iter_over_dicts",
        ),
        param(
            ["???", ListConfig("???"), DictConfig("???")],
            raises(MissingMandatoryValue),
            ["???", ListConfig("???"), DictConfig("???")],
            None,
            id="iter_over_missing",
        ),
        param(
            {
                "defaults": [
                    {"optimizer": "adam"},
                    {"dataset": "imagenet"},
                    {"foo": "${defaults.0.optimizer}_${defaults.1.dataset}"},
                ]
            },
            [
                OmegaConf.create({"optimizer": "adam"}),
                OmegaConf.create({"dataset": "imagenet"}),
                OmegaConf.create({"foo": "adam_imagenet"}),
            ],
            [
                OmegaConf.create({"optimizer": "adam"}),
                OmegaConf.create({"dataset": "imagenet"}),
                OmegaConf.create(
                    {"foo": "${defaults.0.optimizer}_${defaults.1.dataset}"}
                ),
            ],
            "defaults",
            id="str_interpolation",
        ),
    ],
)
def test_iterate_list(
    input_: Any, expected: Any, expected_no_resolve: Any, list_key: str
) -> None:
    c = OmegaConf.create(input_)
    if list_key is not None:
        lst = c.get(list_key)
    else:
        lst = c

    def test_iter(iterator: Any, expected_output: Any) -> None:
        if isinstance(expected_output, list):
            items = [x for x in iterator]
            assert items == expected_output
            for idx in range(len(items)):
                assert type(items[idx]) is type(expected_output[idx])  # noqa
        else:
            with expected_output:
                for _ in iterator:
                    pass

    test_iter(iter(lst), expected)
    test_iter(lst._iter_ex(resolve=False), expected_no_resolve)


def test_iterate_list_with_missing_interpolation() -> None:
    c = OmegaConf.create([1, "${10}"])
    itr = iter(c)
    assert 1 == next(itr)
    with raises(InterpolationKeyError):
        next(itr)


def test_iterate_list_with_missing() -> None:
    c = OmegaConf.create([1, "???"])
    itr = iter(c)
    assert 1 == next(itr)
    with raises(MissingMandatoryValue):
        next(itr)


def test_items_with_interpolation() -> None:
    c = OmegaConf.create(["foo", "${0}"])
    assert c == ["foo", "foo"]


@mark.parametrize(
    ["cfg", "key", "expected_out", "expected_cfg"],
    [
        param([1, 2, 3], 0, 1, [2, 3]),
        param([1, 2, 3], None, 3, [1, 2]),
        param(["???", 2, 3], 0, None, [2, 3]),
        param([1, None, 3], 1, None, [1, 3]),
    ],
)
def test_list_pop(
    cfg: List[Any], key: Optional[int], expected_out: Any, expected_cfg: Any
) -> None:
    c = OmegaConf.create(cfg)
    val = c.pop() if key is None else c.pop(key)
    assert val == expected_out
    assert c == expected_cfg
    validate_list_keys(c)


@mark.parametrize(
    ["cfg", "key", "exc"],
    [
        param([1, 2, 3], 100, IndexError),
        param(["${4}", 2, 3], 0, InterpolationKeyError),
        param(["${1}", "???", 3], 0, InterpolationToMissingValueError),
    ],
)
def test_list_pop_errors(cfg: List[Any], key: int, exc: type) -> None:
    c = OmegaConf.create(cfg)
    with raises(exc):
        c.pop(key)
    assert c == cfg
    validate_list_keys(c)


def test_list_pop_on_unexpected_exception_not_modifying() -> None:
    src = [1, 2, 3, 4]
    c = OmegaConf.create(src)

    with raises(ConfigTypeError):
        c.pop("foo")  # type: ignore
    assert c == src


def test_in_list() -> None:
    c = OmegaConf.create([10, 11, {"a": 12}])
    assert 10 in c
    assert 11 in c
    assert {"a": 12} in c
    assert "blah" not in c


def test_in_with_interpolation() -> None:
    c = OmegaConf.create({"a": ["${b}"], "b": 10})
    assert 10 in c.a


@mark.parametrize(
    ("lst", "expected"),
    [
        param(
            ListConfig(content=None),
            raises(
                TypeError,
                match="Cannot check if an item is in a ListConfig object representing None",
            ),
            id="ListConfig(None)",
        ),
        param(
            ListConfig(content="???"),
            raises(
                MissingMandatoryValue,
                match="Cannot check if an item is in missing ListConfig",
            ),
            id="ListConfig(???)",
        ),
    ],
)
def test_not_in_special_lists(lst: Any, expected: Any) -> None:
    with expected:
        "foo" not in lst


def test_list_config_with_list() -> None:
    c = OmegaConf.create([])
    assert isinstance(c, ListConfig)


def test_list_config_with_tuple() -> None:
    c = OmegaConf.create(())
    assert isinstance(c, ListConfig)


def test_items_on_list() -> None:
    c = OmegaConf.create([1, 2])
    with raises(AttributeError):
        c.items()


def test_list_enumerate() -> None:
    src: List[Optional[str]] = ["a", "b", "c", "d"]
    c = OmegaConf.create(src)
    for i, v in enumerate(c):
        assert src[i] == v
        assert v is not None
        src[i] = None

    for v in src:
        assert v is None


def test_list_delitem() -> None:
    c = OmegaConf.create([1, 2, 3])
    assert c == [1, 2, 3]
    del c[0]
    assert c == [2, 3]
    with raises(IndexError):
        del c[100]

    validate_list_keys(c)


@mark.parametrize(
    "lst,expected",
    [
        (OmegaConf.create([1, 2]), 2),
        (ListConfig(content=None), 0),
        (ListConfig(content="???"), 0),
        (ListConfig(content="${foo}"), 0),
        (ListConfig(content="${foo}", parent=DictConfig({"foo": [1, 2]})), 0),
    ],
)
def test_list_len(lst: Any, expected: Any) -> None:
    assert len(lst) == expected


def test_nested_list_assign_illegal_value() -> None:
    c = OmegaConf.create({"a": [None]})
    with raises(
        UnsupportedValueType,
        match=re.escape(
            dedent(
                """\
                Value 'IllegalType' is not a supported primitive type
                    full_key: a[0]"""
            )
        ),
    ):
        c.a[0] = IllegalType()


def test_list_append() -> None:
    c = OmegaConf.create([])
    c.append(1)
    c.append(2)
    c.append({})
    c.append([])
    assert c == [1, 2, {}, []]

    validate_list_keys(c)


@mark.parametrize(
    "lc,element,err",
    [
        param(
            ListConfig(content=[], element_type=int),
            "foo",
            "Value 'foo' of type 'str' could not be converted to Integer",
            id="append_str_to_list[int]",
        ),
        param(
            ListConfig(content=[], element_type=Color),
            "foo",
            "Invalid value 'foo', expected one of [RED, GREEN, BLUE]",
            id="append_str_to_list[Color]",
        ),
        param(
            ListConfig(content=[], element_type=User),
            "foo",
            "Invalid type assigned: str is not a subclass of User. value: foo",
            id="append_str_to_list[User]",
        ),
        param(
            ListConfig(content=[], element_type=User),
            {"name": "Bond", "age": 7},
            "Invalid type assigned: dict is not a subclass of User. value: {'name': 'Bond', 'age': 7}",
            id="list:convert_dict_to_user",
        ),
        param(
            ListConfig(content=[], element_type=User),
            {},
            "Invalid type assigned: dict is not a subclass of User. value: {}",
            id="list:convert_empty_dict_to_user",
        ),
        param(
            ListConfig(content=[], element_type=List[int]),
            123,
            "Invalid value assigned: int is not a ListConfig, list or tuple.",
        ),
        param(
            ListConfig(content=[], element_type=List[int]),
            None,
            "Invalid type assigned: NoneType is not a subclass of List[int]",
        ),
    ],
)
def test_append_invalid_element_type(lc: ListConfig, element: Any, err: Any) -> None:
    with raises(ValidationError, match=re.escape(err)):
        lc.append(element)


@mark.parametrize(
    "lc,element,expected",
    [
        param(
            ListConfig(content=[], element_type=int),
            "10",
            10,
            id="list:convert_str_to_int",
        ),
        param(
            ListConfig(content=[], element_type=float),
            "10",
            10.0,
            id="list:convert_str_to_float",
        ),
        param(
            ListConfig(content=[], element_type=str),
            10,
            "10",
            id="list:convert_int_to_str",
        ),
        param(
            ListConfig(content=[], element_type=bool),
            "yes",
            True,
            id="list:convert_str_to_bool",
        ),
        param(
            ListConfig(content=[], element_type=Color),
            "RED",
            Color.RED,
            id="list:convert_str_to_enum",
        ),
        param(
            ListConfig(content=[], element_type=Path),
            "hello.txt",
            Path("hello.txt"),
            id="list:convert_str_to_path",
        ),
    ],
)
def test_append_convert(lc: ListConfig, element: Any, expected: Any) -> None:
    lc.append(element)
    value = lc[-1]
    assert value == expected
    assert type(value) == type(expected)


@mark.parametrize(
    "index, expected", [(slice(1, 3), [11, 12]), (slice(0, 3, 2), [10, 12]), (-1, 13)]
)
def test_list_index(index: Any, expected: Any) -> None:
    c = OmegaConf.create([10, 11, 12, 13])
    assert c[index] == expected


@mark.parametrize(
    "cfg, expected",
    [
        (OmegaConf.create([1, 2, 3]), ["0", "1", "2"]),
        (ListConfig(content="???"), []),
        (ListConfig(content=None), []),
    ],
)
def test_list_dir(cfg: Any, expected: Any) -> None:
    assert dir(cfg) == expected


def validate_list_keys(c: Any) -> None:
    # validate keys are maintained
    for i in range(len(c)):
        assert c._get_node(i)._metadata.key == i


@mark.parametrize(
    "cfg, value, expected, expected_ref_type",
    [
        param(
            ListConfig(element_type=int, content=[]),
            123,
            [123],
            int,
            id="typed_list",
        ),
        param(
            ListConfig(element_type=int, content=[]),
            None,
            ValidationError,
            None,
            id="typed_list_append_none",
        ),
        param(
            ListConfig(element_type=Optional[int], content=[]),
            123,
            [123],
            int,
            id="optional_typed_list",
        ),
        param(
            ListConfig(element_type=Optional[int], content=[]),
            None,
            [None],
            int,
            id="optional_typed_list_append_none",
        ),
        param(
            ListConfig(element_type=User, content=[]),
            User(name="bond"),
            [User(name="bond")],
            User,
            id="user_list",
        ),
        param(
            ListConfig(element_type=User, content=[]),
            None,
            ValidationError,
            None,
            id="user_list_append_none",
        ),
        param(
            ListConfig(element_type=Optional[User], content=[]),
            User(name="bond"),
            [User(name="bond")],
            User,
            id="optional_user_list",
        ),
        param(
            ListConfig(element_type=Optional[User], content=[]),
            None,
            [None],
            User,
            id="optional_user_list_append_none",
        ),
    ],
)
def test_append_to_typed(
    cfg: ListConfig,
    value: Any,
    expected: Any,
    expected_ref_type: type,
) -> None:
    cfg = _ensure_container(cfg)
    if isinstance(expected, type):
        with raises(expected):
            cfg.append(value)
    else:
        cfg.append(value)
        assert cfg == expected
        node = cfg._get_node(-1)
        assert isinstance(node, Node)
        assert node._metadata.ref_type == expected_ref_type
        validate_list_keys(cfg)


@mark.parametrize(
    "input_, index, value, expected, expected_node_type, expectation",
    [
        (["a", "b", "c"], 1, 100, ["a", 100, "b", "c"], AnyNode, None),
        (
            ["a", "b", "c"],
            1,
            IntegerNode(100),
            ["a", 100, "b", "c"],
            IntegerNode,
            None,
        ),
        (["a", "b", "c"], 1, "foo", ["a", "foo", "b", "c"], AnyNode, None),
        (
            ["a", "b", "c"],
            1,
            StringNode("foo"),
            ["a", "foo", "b", "c"],
            StringNode,
            None,
        ),
        (
            ListConfig(element_type=int, content=[]),
            0,
            "foo",
            None,
            None,
            ValidationError,
        ),
        param(
            ListConfig(element_type=int, content=[]),
            0,
            123,
            [123],
            IntegerNode,
            None,
            id="typed_list",
        ),
        param(
            ListConfig(element_type=int, content=[]),
            0,
            None,
            None,
            None,
            ValidationError,
            id="typed_list_insert_none",
        ),
    ],
)
def test_insert(
    input_: List[str],
    index: int,
    value: Any,
    expected: Any,
    expected_node_type: type,
    expectation: Any,
) -> None:
    c = OmegaConf.create(input_)
    if expectation is None:
        c.insert(index, value)
        assert c == expected
        assert type(c._get_node(index)) == expected_node_type
    else:
        with raises(expectation):
            c.insert(index, value)
    validate_list_keys(c)


@mark.parametrize(
    "lst,idx,value,expectation",
    [
        (ListConfig(content=None), 0, 10, raises(TypeError)),
        (ListConfig(content="???"), 0, 10, raises(MissingMandatoryValue)),
    ],
)
def test_insert_special_list(lst: Any, idx: Any, value: Any, expectation: Any) -> None:
    with expectation:
        lst.insert(idx, value)


@mark.parametrize(
    "src, append, result",
    [
        ([], [], []),
        ([1, 2], [3], [1, 2, 3]),
        ([1, 2], ("a", "b", "c"), [1, 2, "a", "b", "c"]),
    ],
)
def test_extend(src: List[Any], append: List[Any], result: List[Any]) -> None:
    lst = OmegaConf.create(src)
    lst.extend(append)
    assert lst == result


@mark.parametrize(
    "src, remove, result, expectation",
    [
        ([10], 10, [], nullcontext()),
        ([], "oops", None, raises(ValueError)),
        ([0, dict(a="blah"), 10], dict(a="blah"), [0, 10], nullcontext()),
        ([1, 2, 1, 2], 2, [1, 1, 2], nullcontext()),
    ],
)
def test_remove(src: List[Any], remove: Any, result: Any, expectation: Any) -> None:
    with expectation:
        lst = OmegaConf.create(src)
        assert isinstance(lst, ListConfig)
        lst.remove(remove)
        assert lst == result


@mark.parametrize("src", [[], [1, 2, 3], [None, dict(foo="bar")]])
@mark.parametrize("num_clears", [1, 2])
def test_clear(src: List[Any], num_clears: int) -> None:
    lst = OmegaConf.create(src)
    for i in range(num_clears):
        lst.clear()
    assert lst == []


@mark.parametrize(
    "src, item, expected_index, expectation",
    [
        ([], 20, -1, raises(ValueError)),
        ([10, 20], 10, 0, nullcontext()),
        ([10, 20], 20, 1, nullcontext()),
    ],
)
def test_index(
    src: List[Any], item: Any, expected_index: int, expectation: Any
) -> None:
    with expectation:
        lst = OmegaConf.create(src)
        assert lst.index(item) == expected_index


def test_index_with_range() -> None:
    lst = OmegaConf.create([10, 20, 30, 40, 50])
    assert lst.index(x=30) == 2
    assert lst.index(x=30, start=1) == 2
    assert lst.index(x=30, start=1, end=3) == 2
    with raises(ValueError):
        lst.index(x=30, start=3)

    with raises(ValueError):
        lst.index(x=30, end=2)


@mark.parametrize(
    "src, item, count",
    [([], 10, 0), ([10], 10, 1), ([10, 2, 10], 10, 2), ([10, 2, 10], None, 0)],
)
def test_count(src: List[Any], item: Any, count: int) -> None:
    lst = OmegaConf.create(src)
    assert lst.count(item) == count


def test_sort() -> None:
    c = OmegaConf.create(["bbb", "aa", "c"])
    c.sort()
    assert ["aa", "bbb", "c"] == c
    c.sort(reverse=True)
    assert ["c", "bbb", "aa"] == c
    c.sort(key=len)
    assert ["c", "aa", "bbb"] == c
    c.sort(key=len, reverse=True)
    assert ["bbb", "aa", "c"] == c


def test_insert_throws_not_changing_list() -> None:
    c = OmegaConf.create([])
    iv = IllegalType()
    with raises(ValueError):
        c.insert(0, iv)
    assert len(c) == 0
    assert c == []

    with flag_override(c, "allow_objects", True):
        c.insert(0, iv)
    assert c == [iv]


def test_append_throws_not_changing_list() -> None:
    c = OmegaConf.create([])
    iv = IllegalType()
    with raises(ValueError):
        c.append(iv)
    assert len(c) == 0
    assert c == []
    validate_list_keys(c)

    with flag_override(c, "allow_objects", True):
        c.append(iv)
    assert c == [iv]


def test_hash() -> None:
    c1 = OmegaConf.create([10])
    c2 = OmegaConf.create([10])
    assert hash(c1) == hash(c2)
    c2[0] = 20
    assert hash(c1) != hash(c2)


@mark.parametrize(
    "in_list1, in_list2,in_expected",
    [
        ([], [], []),
        ([1, 2], [3, 4], [1, 2, 3, 4]),
        (["x", 2, "${0}"], [5, 6, 7], ["x", 2, "x", 5, 6, 7]),
    ],
)
class TestListAdd:
    @mark.parametrize(
        "left_listconfig, right_listconfig",
        [
            param(True, True, id="listconfig_plus_listconfig"),
            param(True, False, id="listconfig_plus_list"),
            param(False, True, id="list_plus_listconfig"),
        ],
    )
    def test_list_plus(
        self,
        in_list1: List[Any],
        in_list2: List[Any],
        in_expected: List[Any],
        left_listconfig: bool,
        right_listconfig: bool,
    ) -> None:
        list1: Union[List[Any], ListConfig] = (
            OmegaConf.create(in_list1) if left_listconfig else in_list1
        )
        list2: Union[List[Any], ListConfig] = (
            OmegaConf.create(in_list2) if right_listconfig else in_list2
        )
        expected = OmegaConf.create(in_expected)
        ret = list1 + list2
        assert ret == expected

    def test_list_plus_eq(
        self, in_list1: List[Any], in_list2: List[Any], in_expected: List[Any]
    ) -> None:
        list1 = OmegaConf.create(in_list1)
        list2 = OmegaConf.create(in_list2)
        expected = OmegaConf.create(in_expected)
        list1 += list2
        assert list1 == expected


def test_deep_add() -> None:
    cfg = OmegaConf.create({"foo": [1, 2, "${bar}"], "bar": "xx"})
    lst = cfg.foo + [10, 20]
    assert lst == [1, 2, "xx", 10, 20]


def test_deep_radd() -> None:
    cfg = OmegaConf.create({"foo": [1, 2, "${bar}"], "bar": "xx"})
    lst = [10, 20] + cfg.foo
    assert lst == [10, 20, 1, 2, "xx"]


def test_set_with_invalid_key() -> None:
    cfg = OmegaConf.create([1, 2, 3])
    with raises(KeyValidationError):
        cfg["foo"] = 4  # type: ignore


@mark.parametrize(
    "lst,idx,expected",
    [
        (OmegaConf.create([1, 2]), 0, 1),
        (ListConfig(content=None), 0, TypeError),
        (ListConfig(content="???"), 0, MissingMandatoryValue),
    ],
)
def test_getitem(lst: Any, idx: Any, expected: Any) -> None:
    if isinstance(expected, type):
        with raises(expected):
            lst.__getitem__(idx)
    else:
        assert lst.__getitem__(idx) == expected


@mark.parametrize(
    "sli",
    [
        (slice(None, None, None)),
        (slice(1, None, None)),
        (slice(-1, None, None)),
        (slice(None, 1, None)),
        (slice(None, -1, None)),
        (slice(None, None, 1)),
        (slice(None, None, -1)),
        (slice(1, None, -2)),
        (slice(None, 1, -2)),
        (slice(1, 3, -1)),
        (slice(3, 1, -1)),
    ],
)
def test_getitem_slice(sli: slice) -> None:
    lst = [1, 2, 3]
    olst = OmegaConf.create([1, 2, 3])
    expected = lst[sli.start : sli.stop : sli.step]
    assert olst.__getitem__(sli) == expected


@mark.parametrize(
    "constructor",
    [OmegaConf.create, list, lambda lst: OmegaConf.create({"foo": lst}).foo],
)
@mark.parametrize(
    "lst, idx, value, expected",
    [
        param(
            ["a", "b", "c", "d"],
            slice(1, 3),
            ["x", "y"],
            ["a", "x", "y", "d"],
            id="same-number-of-elements",
        ),
        param(
            ["a", "x", "y", "d"],
            slice(1, 3),
            ["x", "y", "z"],
            ["a", "x", "y", "z", "d"],
            id="extra-elements",
        ),
        param(
            ["a", "x", "y", "z", "d"],
            slice(1, 1),
            ["b"],
            ["a", "b", "x", "y", "z", "d"],
            id="insert only",
        ),
        param(
            ["a", "b", "x", "y", "z", "d"],
            slice(1, 1),
            [],
            ["a", "b", "x", "y", "z", "d"],
            id="nop",
        ),
        param(
            ["a", "b", "x", "y", "z", "d"],
            slice(1, 3),
            [],
            ["a", "y", "z", "d"],
            id="less-elements",
        ),
        param(
            ["a", "y", "z", "d"],
            slice(1, 2, 1),
            ["b"],
            ["a", "b", "z", "d"],
            id="extended-slice",
        ),
        param(
            ["a", "b", "c", "d"],
            slice(1, 3, 1),
            ["x", "y"],
            ["a", "x", "y", "d"],
            id="extended-slice2",
        ),
        param(
            ["a", "b", "z", "d"],
            slice(0, 3, 2),
            ["a", "c"],
            ["a", "b", "c", "d"],
            id="extended-slice-disjoint",
        ),
        param(
            ["a", "b", "c", "d"],
            slice(1, 3),
            1,
            raises(TypeError),
            id="non-iterable-input",
        ),
        param(
            ["a", "b", "c", "d"],
            slice(1, 3),
            [object()],
            raises(UnsupportedValueType),
            id="partially-valid-input",
        ),
        param(
            ["a", "b", "c", "d"],
            slice(1, 3, 1),
            ["x", "y", "z"],
            ["a", "x", "y", "z", "d"],
            id="extended-slice-length-mismatch",
        ),
        param(
            ["a", "b", "c", "d", "e", "f"],
            slice(1, 5, 2),
            ["x", "y", "z"],
            raises(ValueError),
            id="extended-slice-length-mismatch2",
        ),
        param(
            ["a", "b", "c", "d", "e", "f"],
            slice(-1, -3, -1),
            ["F", "E"],
            ["a", "b", "c", "d", "E", "F"],
            id="extended-slice-reverse",
        ),
        param(
            ["a", "b", "c", "d", "e", "g"],
            slice(-1, -3, None),
            ["f"],
            ["a", "b", "c", "d", "e", "f", "g"],
            id="slice-reverse-insert",
        ),
        param(
            ["a", "b", "c", "r", "r", "e"],
            slice(-3, -1, None),
            ["d"],
            ["a", "b", "c", "d", "e"],
            id="slice-reverse-replace",
        ),
        param(
            ["c", "d"],
            slice(-10, -10, None),
            ["a", "b"],
            ["a", "b", "c", "d"],
            id="slice-reverse-insert-underflow",
        ),
        param(
            ["a", "b"],
            slice(10, 10, None),
            ["c", "d"],
            ["a", "b", "c", "d"],
            id="slice-reverse-insert-overflow",
        ),
    ],
)
def test_setitem_slice(
    lst: List[Any],
    idx: slice,
    value: Union[List[Any], Any],
    expected: Union[List[Any], Any],
    constructor: Callable[[List[Any]], MutableSequence[Any]],
) -> None:
    cfg = constructor(lst)
    if isinstance(expected, list):
        cfg[idx] = value
        assert cfg == expected
    else:
        expected_exception: Any = getattr(expected, 'expected_exception', None)

        # Special case: plain Python list cannot raise OmegaConf-specific exceptions
        if (constructor is list and
            expected_exception is not None and
            isinstance(expected_exception, type)):

            if issubclass(expected_exception, OmegaConfBaseException):
                # Plain Python list should accept the value - this is correct behavior
                cfg[idx] = value
                return

        orig_cfg = cfg[:]
        with expected:  # type: ignore
            cfg[idx] = value
        assert cfg == orig_cfg


@mark.parametrize(
    "lst,idx,expected",
    [
        (OmegaConf.create([1, 2]), 0, 1),
        (OmegaConf.create([1, 2]), "foo", KeyValidationError),
        (OmegaConf.create([1, "${2}"]), 1, InterpolationKeyError),
        (OmegaConf.create(["???", "${0}"]), 1, InterpolationToMissingValueError),
        (ListConfig(content=None), 0, TypeError),
        (ListConfig(content="???"), 0, MissingMandatoryValue),
    ],
)
def test_get(lst: Any, idx: Any, expected: Any) -> None:
    if isinstance(expected, type):
        with raises(expected):
            lst.get(idx)
    else:
        assert lst.__getitem__(idx) == expected


def test_getattr() -> None:
    src = ["a", "b", "c"]
    cfg = OmegaConf.create(src)
    with raises(AttributeError):
        getattr(cfg, "foo")
    assert getattr(cfg, "0") == src[0]
    assert getattr(cfg, "1") == src[1]
    assert getattr(cfg, "2") == src[2]


def test_shallow_copy() -> None:
    cfg = OmegaConf.create([1, 2])
    c = cfg.copy()
    assert cfg == c
    cfg[0] = 42
    assert cfg[0] == 42
    assert c[0] == 1


def test_shallow_copy_missing() -> None:
    cfg = ListConfig(content=MISSING)
    c = cfg.copy()
    c._set_value([1])
    assert c[0] == 1
    assert cfg._is_missing()


def test_shallow_copy_none() -> None:
    cfg = ListConfig(content=None)
    c = cfg.copy()
    c._set_value([1])
    assert c[0] == 1
    assert cfg._is_none()


@mark.parametrize("flag", ["struct", "readonly"])
def test_listconfig_creation_with_parent_flag(flag: str) -> None:
    parent = OmegaConf.create([])
    parent._set_flag(flag, True)
    d = [1, 2, 3]
    cfg = ListConfig(d, parent=parent)
    assert cfg == d


@mark.parametrize(
    "node",
    [
        param(AnyNode("hello"), id="any"),
        param(DictConfig({}), id="dict"),
        param(ListConfig([]), id="list"),
    ],
)
def test_node_copy_on_append(node: Any) -> None:
    cfg = OmegaConf.create([])
    cfg.append(node)
    assert cfg.__dict__["_content"][0] is not node


@mark.parametrize(
    "cfg,key,value,error",
    [
        param(
            ListConfig([], element_type=Optional[User]),
            0,
            "foo",
            True,
            id="structured:set_optional_to_bad_type",
        ),
        param(
            ListConfig([], element_type=int),
            0,
            None,
            True,
            id="set_to_none_raises",
        ),
        param(
            ListConfig([], element_type=Optional[int]),
            0,
            None,
            False,
            id="optional_set_to_none",
        ),
    ],
)
def test_validate_set(cfg: ListConfig, key: int, value: Any, error: bool) -> None:
    if error:
        with raises(ValidationError):
            cfg._validate_set(key, value)
    else:
        cfg._validate_set(key, value)
