import io
import pickle
import tempfile
from contextlib import contextmanager
from copy import copy
from copy import deepcopy

import pytest

from werkzeug import datastructures as ds
from werkzeug import http
from werkzeug.datastructures.structures import _ImmutableOrderedMultiDict
from werkzeug.datastructures.structures import _OrderedMultiDict
from werkzeug.exceptions import BadRequestKeyError


class TestNativeItermethods:
    def test_basic(self):
        class StupidDict:
            def keys(self, multi=1):
                return iter(["a", "b", "c"] * multi)

            def values(self, multi=1):
                return iter([1, 2, 3] * multi)

            def items(self, multi=1):
                return iter(
                    zip(iter(self.keys(multi=multi)), iter(self.values(multi=multi)))
                )

        d = StupidDict()
        expected_keys = ["a", "b", "c"]
        expected_values = [1, 2, 3]
        expected_items = list(zip(expected_keys, expected_values))

        assert list(d.keys()) == expected_keys
        assert list(d.values()) == expected_values
        assert list(d.items()) == expected_items

        assert list(d.keys(2)) == expected_keys * 2
        assert list(d.values(2)) == expected_values * 2
        assert list(d.items(2)) == expected_items * 2


class _MutableMultiDictTests:
    storage_class: type["ds.MultiDict"]

    def test_pickle(self):
        cls = self.storage_class

        def create_instance(module=None):
            if module is None:
                d = cls()
            else:
                old = cls.__module__
                cls.__module__ = module
                d = cls()
                cls.__module__ = old
            d.setlist(b"foo", [1, 2, 3, 4])
            d.setlist(b"bar", b"foo bar baz".split())
            return d

        for protocol in range(pickle.HIGHEST_PROTOCOL + 1):
            d = create_instance()
            s = pickle.dumps(d, protocol)
            ud = pickle.loads(s)
            assert type(ud) == type(d)  # noqa: E721
            assert ud == d
            alternative = pickle.dumps(create_instance("werkzeug"), protocol)
            assert pickle.loads(alternative) == d
            ud[b"newkey"] = b"bla"
            assert ud != d

    def test_multidict_dict_interop(self):
        # https://github.com/pallets/werkzeug/pull/2043
        md = self.storage_class([("a", 1), ("a", 2)])
        assert dict(md)["a"] != [1, 2]
        assert dict(md)["a"] == 1
        assert dict(md) == {**md} == {"a": 1}

    def test_basic_interface(self):
        md = self.storage_class()
        assert isinstance(md, dict)

        mapping = [
            ("a", 1),
            ("b", 2),
            ("a", 2),
            ("d", 3),
            ("a", 1),
            ("a", 3),
            ("d", 4),
            ("c", 3),
        ]
        md = self.storage_class(mapping)

        # simple getitem gives the first value
        assert md["a"] == 1
        assert md["c"] == 3
        with pytest.raises(KeyError):
            md["e"]
        assert md.get("a") == 1

        # list getitem
        assert md.getlist("a") == [1, 2, 1, 3]
        assert md.getlist("d") == [3, 4]
        # do not raise if key not found
        assert md.getlist("x") == []

        # simple setitem overwrites all values
        md["a"] = 42
        assert md.getlist("a") == [42]

        # list setitem
        md.setlist("a", [1, 2, 3])
        assert md["a"] == 1
        assert md.getlist("a") == [1, 2, 3]

        # verify that it does not change original lists
        l1 = [1, 2, 3]
        md.setlist("a", l1)
        del l1[:]
        assert md["a"] == 1

        # setdefault, setlistdefault
        assert md.setdefault("u", 23) == 23
        assert md.getlist("u") == [23]
        del md["u"]

        md.setlist("u", [-1, -2])

        # delitem
        del md["u"]
        with pytest.raises(KeyError):
            md["u"]
        del md["d"]
        assert md.getlist("d") == []

        # keys, values, items, lists
        assert list(sorted(md.keys())) == ["a", "b", "c"]
        assert list(sorted(md.keys())) == ["a", "b", "c"]

        assert list(sorted(md.values())) == [1, 2, 3]
        assert list(sorted(md.values())) == [1, 2, 3]

        assert list(sorted(md.items())) == [("a", 1), ("b", 2), ("c", 3)]
        assert list(sorted(md.items(multi=True))) == [
            ("a", 1),
            ("a", 2),
            ("a", 3),
            ("b", 2),
            ("c", 3),
        ]
        assert list(sorted(md.items())) == [("a", 1), ("b", 2), ("c", 3)]
        assert list(sorted(md.items(multi=True))) == [
            ("a", 1),
            ("a", 2),
            ("a", 3),
            ("b", 2),
            ("c", 3),
        ]

        assert list(sorted(md.lists())) == [("a", [1, 2, 3]), ("b", [2]), ("c", [3])]
        assert list(sorted(md.lists())) == [("a", [1, 2, 3]), ("b", [2]), ("c", [3])]

        # copy method
        c = md.copy()
        assert c["a"] == 1
        assert c.getlist("a") == [1, 2, 3]

        # copy method 2
        c = copy(md)
        assert c["a"] == 1
        assert c.getlist("a") == [1, 2, 3]

        # deepcopy method
        c = md.deepcopy()
        assert c["a"] == 1
        assert c.getlist("a") == [1, 2, 3]

        # deepcopy method 2
        c = deepcopy(md)
        assert c["a"] == 1
        assert c.getlist("a") == [1, 2, 3]

        # update with a multidict
        od = self.storage_class([("a", 4), ("a", 5), ("y", 0)])
        md.update(od)
        assert md.getlist("a") == [1, 2, 3, 4, 5]
        assert md.getlist("y") == [0]

        # update with a regular dict
        md = c
        od = {"a": 4, "y": 0}
        md.update(od)
        assert md.getlist("a") == [1, 2, 3, 4]
        assert md.getlist("y") == [0]

        # pop, poplist, popitem, popitemlist
        assert md.pop("y") == 0
        assert "y" not in md
        assert md.poplist("a") == [1, 2, 3, 4]
        assert "a" not in md
        assert md.poplist("missing") == []

        # remaining: b=2, c=3
        popped = md.popitem()
        assert popped in [("b", 2), ("c", 3)]
        popped = md.popitemlist()
        assert popped in [("b", [2]), ("c", [3])]

        # type conversion
        md = self.storage_class({"a": "4", "b": ["2", "3"]})
        assert md.get("a", type=int) == 4
        assert md.getlist("b", type=int) == [2, 3]

        # repr
        md = self.storage_class([("a", 1), ("a", 2), ("b", 3)])
        assert "('a', 1)" in repr(md)
        assert "('a', 2)" in repr(md)
        assert "('b', 3)" in repr(md)

        # add and getlist
        md.add("c", "42")
        md.add("c", "23")
        assert md.getlist("c") == ["42", "23"]
        md.add("c", "blah")
        assert md.getlist("c", type=int) == [42, 23]

        # setdefault
        md = self.storage_class()
        md.setdefault("x", []).append(42)
        md.setdefault("x", []).append(23)
        assert md["x"] == [42, 23]

        # to dict
        md = self.storage_class()
        md["foo"] = 42
        md.add("bar", 1)
        md.add("bar", 2)
        assert md.to_dict() == {"foo": 42, "bar": 1}
        assert md.to_dict(flat=False) == {"foo": [42], "bar": [1, 2]}

        # popitem from empty dict
        with pytest.raises(KeyError):
            self.storage_class().popitem()

        with pytest.raises(KeyError):
            self.storage_class().popitemlist()

        # key errors are of a special type
        with pytest.raises(BadRequestKeyError):
            self.storage_class()[42]

        # setlist works
        md = self.storage_class()
        md["foo"] = 42
        md.setlist("foo", [1, 2])
        assert md.getlist("foo") == [1, 2]

    def test_or(self) -> None:
        a = self.storage_class({"x": 1})
        b = a | {"y": 2}
        assert isinstance(b, self.storage_class)
        assert "x" in b and "y" in b

    def test_ior(self) -> None:
        a = self.storage_class({"x": 1})
        a |= {"y": 2}
        assert "x" in a and "y" in a


class _ImmutableDictTests:
    storage_class: type[dict]

    def test_follows_dict_interface(self):
        cls = self.storage_class

        data = {"foo": 1, "bar": 2, "baz": 3}
        d = cls(data)

        assert d["foo"] == 1
        assert d["bar"] == 2
        assert d["baz"] == 3
        assert sorted(d.keys()) == ["bar", "baz", "foo"]
        assert "foo" in d
        assert "foox" not in d
        assert len(d) == 3

    def test_copies_are_mutable(self):
        cls = self.storage_class
        immutable = cls({"a": 1})
        with pytest.raises(TypeError):
            immutable.pop("a")

        mutable = immutable.copy()
        mutable.pop("a")
        assert "a" in immutable
        assert mutable is not immutable
        assert copy(immutable) is immutable

    def test_dict_is_hashable(self):
        cls = self.storage_class
        immutable = cls({"a": 1, "b": 2})
        immutable2 = cls({"a": 2, "b": 2})
        x = {immutable}
        assert immutable in x
        assert immutable2 not in x
        x.discard(immutable)
        assert immutable not in x
        assert immutable2 not in x
        x.add(immutable2)
        assert immutable not in x
        assert immutable2 in x
        x.add(immutable)
        assert immutable in x
        assert immutable2 in x

    def test_or(self) -> None:
        a = self.storage_class({"x": 1})
        b = a | {"y": 2}
        assert "x" in b and "y" in b

    def test_ior(self) -> None:
        a = self.storage_class({"x": 1})

        with pytest.raises(TypeError):
            a |= {"y": 2}


class TestImmutableTypeConversionDict(_ImmutableDictTests):
    storage_class = ds.ImmutableTypeConversionDict


class TestImmutableMultiDict(_ImmutableDictTests):
    storage_class = ds.ImmutableMultiDict

    def test_multidict_is_hashable(self):
        cls = self.storage_class
        immutable = cls({"a": [1, 2], "b": 2})
        immutable2 = cls({"a": [1], "b": 2})
        x = {immutable}
        assert immutable in x
        assert immutable2 not in x
        x.discard(immutable)
        assert immutable not in x
        assert immutable2 not in x
        x.add(immutable2)
        assert immutable not in x
        assert immutable2 in x
        x.add(immutable)
        assert immutable in x
        assert immutable2 in x


class TestImmutableDict(_ImmutableDictTests):
    storage_class = ds.ImmutableDict


@pytest.mark.filterwarnings("ignore:'OrderedMultiDict':DeprecationWarning")
class TestImmutableOrderedMultiDict(_ImmutableDictTests):
    storage_class = _ImmutableOrderedMultiDict

    def test_ordered_multidict_is_hashable(self):
        a = self.storage_class([("a", 1), ("b", 1), ("a", 2)])
        b = self.storage_class([("a", 1), ("a", 2), ("b", 1)])
        assert hash(a) != hash(b)


class TestMultiDict(_MutableMultiDictTests):
    storage_class = ds.MultiDict

    def test_multidict_pop(self):
        def make_d():
            return self.storage_class({"foo": [1, 2, 3, 4]})

        d = make_d()
        assert d.pop("foo") == 1
        assert not d
        d = make_d()
        assert d.pop("foo", 32) == 1
        assert not d
        d = make_d()
        assert d.pop("foos", 32) == 32
        assert d

        with pytest.raises(KeyError):
            d.pop("foos")

    def test_multidict_pop_raise_badrequestkeyerror_for_empty_list_value(self):
        mapping = [("a", "b"), ("a", "c")]
        md = self.storage_class(mapping)

        md.setlistdefault("empty", [])

        with pytest.raises(KeyError):
            md.pop("empty")

    def test_multidict_popitem_raise_badrequestkeyerror_for_empty_list_value(self):
        mapping = []
        md = self.storage_class(mapping)

        md.setlistdefault("empty", [])

        with pytest.raises(BadRequestKeyError):
            md.popitem()

    def test_setlistdefault(self):
        md = self.storage_class()
        assert md.setlistdefault("u", [-1, -2]) == [-1, -2]
        assert md.getlist("u") == [-1, -2]
        assert md["u"] == -1

    def test_iter_interfaces(self):
        mapping = [
            ("a", 1),
            ("b", 2),
            ("a", 2),
            ("d", 3),
            ("a", 1),
            ("a", 3),
            ("d", 4),
            ("c", 3),
        ]
        md = self.storage_class(mapping)
        assert list(zip(md.keys(), md.listvalues())) == list(md.lists())
        assert list(zip(md, md.listvalues())) == list(md.lists())
        assert list(zip(md.keys(), md.listvalues())) == list(md.lists())

    def test_getitem_raise_badrequestkeyerror_for_empty_list_value(self):
        mapping = [("a", "b"), ("a", "c")]
        md = self.storage_class(mapping)

        md.setlistdefault("empty", [])

        with pytest.raises(KeyError):
            md["empty"]


@pytest.mark.filterwarnings("ignore:'OrderedMultiDict':DeprecationWarning")
class TestOrderedMultiDict(_MutableMultiDictTests):
    storage_class = _OrderedMultiDict

    def test_ordered_interface(self):
        cls = self.storage_class

        d = cls()
        assert not d
        d.add("foo", "bar")
        assert len(d) == 1
        d.add("foo", "baz")
        assert len(d) == 1
        assert list(d.items()) == [("foo", "bar")]
        assert list(d) == ["foo"]
        assert list(d.items(multi=True)) == [("foo", "bar"), ("foo", "baz")]
        del d["foo"]
        assert not d
        assert len(d) == 0
        assert list(d) == []

        d.update([("foo", 1), ("foo", 2), ("bar", 42)])
        d.add("foo", 3)
        assert d.getlist("foo") == [1, 2, 3]
        assert d.getlist("bar") == [42]
        assert list(d.items()) == [("foo", 1), ("bar", 42)]

        expected = ["foo", "bar"]

        assert list(d.keys()) == expected
        assert list(d) == expected
        assert list(d.keys()) == expected

        assert list(d.items(multi=True)) == [
            ("foo", 1),
            ("foo", 2),
            ("bar", 42),
            ("foo", 3),
        ]
        assert len(d) == 2

        assert d.pop("foo") == 1
        assert d.pop("blafasel", None) is None
        assert d.pop("blafasel", 42) == 42
        assert len(d) == 1
        assert d.poplist("bar") == [42]
        assert not d

        assert d.get("missingkey") is None

        d.add("foo", 42)
        d.add("foo", 23)
        d.add("bar", 2)
        d.add("foo", 42)
        assert d == ds.MultiDict(d)
        id = self.storage_class(d)
        assert d == id
        d.add("foo", 2)
        assert d != id

        d.update({"blah": [1, 2, 3]})
        assert d["blah"] == 1
        assert d.getlist("blah") == [1, 2, 3]

        # setlist works
        d = self.storage_class()
        d["foo"] = 42
        d.setlist("foo", [1, 2])
        assert d.getlist("foo") == [1, 2]
        with pytest.raises(BadRequestKeyError):
            d.pop("missing")

        with pytest.raises(BadRequestKeyError):
            d["missing"]

        # popping
        d = self.storage_class()
        d.add("foo", 23)
        d.add("foo", 42)
        d.add("foo", 1)
        assert d.popitem() == ("foo", 23)
        with pytest.raises(BadRequestKeyError):
            d.popitem()
        assert not d

        d.add("foo", 23)
        d.add("foo", 42)
        d.add("foo", 1)
        assert d.popitemlist() == ("foo", [23, 42, 1])

        with pytest.raises(BadRequestKeyError):
            d.popitemlist()

        # Unhashable
        d = self.storage_class()
        d.add("foo", 23)
        pytest.raises(TypeError, hash, d)

    def test_iterables(self):
        a = ds.MultiDict((("key_a", "value_a"),))
        b = ds.MultiDict((("key_b", "value_b"),))
        ab = ds.CombinedMultiDict((a, b))

        assert sorted(ab.lists()) == [("key_a", ["value_a"]), ("key_b", ["value_b"])]
        assert sorted(ab.listvalues()) == [["value_a"], ["value_b"]]
        assert sorted(ab.keys()) == ["key_a", "key_b"]

        assert sorted(ab.lists()) == [("key_a", ["value_a"]), ("key_b", ["value_b"])]
        assert sorted(ab.listvalues()) == [["value_a"], ["value_b"]]
        assert sorted(ab.keys()) == ["key_a", "key_b"]

    def test_get_description(self):
        data = ds.OrderedMultiDict()

        with pytest.raises(BadRequestKeyError) as exc_info:
            data["baz"]

        assert "baz" not in exc_info.value.get_description()
        exc_info.value.show_exception = True
        assert "baz" in exc_info.value.get_description()

        with pytest.raises(BadRequestKeyError) as exc_info:
            data.pop("baz")

        exc_info.value.show_exception = True
        assert "baz" in exc_info.value.get_description()
        exc_info.value.args = ()
        assert "baz" not in exc_info.value.get_description()


class TestTypeConversionDict:
    storage_class = ds.TypeConversionDict

    def test_value_conversion(self):
        d = self.storage_class(foo="1")
        assert d.get("foo", type=int) == 1

    def test_return_default_when_conversion_is_not_possible(self):
        d = self.storage_class(foo="bar", baz=None)
        assert d.get("foo", default=-1, type=int) == -1
        assert d.get("baz", default=-1, type=int) == -1

    def test_propagate_exceptions_in_conversion(self):
        d = self.storage_class(foo="bar")
        switch = {"a": 1}
        with pytest.raises(KeyError):
            d.get("foo", type=lambda x: switch[x])


class TestCombinedMultiDict:
    storage_class = ds.CombinedMultiDict

    def test_basic_interface(self):
        d1 = ds.MultiDict([("foo", "1")])
        d2 = ds.MultiDict([("bar", "2"), ("bar", "3")])
        d = self.storage_class([d1, d2])

        # lookup
        assert d["foo"] == "1"
        assert d["bar"] == "2"
        assert d.getlist("bar") == ["2", "3"]

        assert sorted(d.items()) == [("bar", "2"), ("foo", "1")]
        assert sorted(d.items(multi=True)) == [("bar", "2"), ("bar", "3"), ("foo", "1")]
        assert "missingkey" not in d
        assert "foo" in d

        # type lookup
        assert d.get("foo", type=int) == 1
        assert d.getlist("bar", type=int) == [2, 3]

        # get key errors for missing stuff
        with pytest.raises(KeyError):
            d["missing"]

        # make sure that they are immutable
        with pytest.raises(TypeError):
            d["foo"] = "blub"

        # copies are mutable
        d = d.copy()
        d["foo"] = "blub"

        # make sure lists merges
        md1 = ds.MultiDict((("foo", "bar"), ("foo", "baz")))
        md2 = ds.MultiDict((("foo", "blafasel"),))
        x = self.storage_class((md1, md2))
        assert list(x.lists()) == [("foo", ["bar", "baz", "blafasel"])]

        # make sure dicts are created properly
        assert x.to_dict() == {"foo": "bar"}
        assert x.to_dict(flat=False) == {"foo": ["bar", "baz", "blafasel"]}

    def test_length(self):
        d1 = ds.MultiDict([("foo", "1")])
        d2 = ds.MultiDict([("bar", "2")])
        assert len(d1) == len(d2) == 1
        d = self.storage_class([d1, d2])
        assert len(d) == 2
        d1.clear()
        assert len(d1) == 0
        assert len(d) == 1


class TestHeaders:
    storage_class = ds.Headers

    def test_basic_interface(self):
        headers = self.storage_class()
        headers.add("Content-Type", "text/plain")
        headers.add("X-Foo", "bar")
        assert "x-Foo" in headers
        assert "Content-type" in headers

        with pytest.raises(ValueError):
            headers.add("X-Example", "foo\r\n bar")

        headers["Content-Type"] = "foo/bar"
        assert headers["Content-Type"] == "foo/bar"
        assert len(headers.getlist("Content-Type")) == 1

        # list conversion
        assert headers.to_wsgi_list() == [("Content-Type", "foo/bar"), ("X-Foo", "bar")]
        assert str(headers) == "Content-Type: foo/bar\r\nX-Foo: bar\r\n\r\n"
        assert str(self.storage_class()) == "\r\n"

        # extended add
        headers.add("Content-Disposition", "attachment", filename="foo")
        assert headers["Content-Disposition"] == "attachment; filename=foo"

        headers.add("x", "y", z='"')
        assert headers["x"] == r'y; z="\""'

        # string conversion
        headers.add("a", 1)
        assert headers["a"] == "1"

    def test_defaults_and_conversion(self):
        # defaults
        headers = self.storage_class(
            [
                ("Content-Type", "text/plain"),
                ("X-Foo", "bar"),
                ("X-Bar", "1"),
                ("X-Bar", "2"),
            ]
        )
        assert headers.getlist("x-bar") == ["1", "2"]
        assert headers.get("x-Bar") == "1"
        assert headers.get("Content-Type") == "text/plain"

        assert headers.setdefault("X-Foo", "nope") == "bar"
        assert headers.setdefault("X-Bar", "nope") == "1"
        assert headers.setdefault("X-Baz", "quux") == "quux"
        assert headers.setdefault("X-Baz", "nope") == "quux"
        headers.pop("X-Baz")

        # newlines are not allowed in values
        with pytest.raises(ValueError):
            self.storage_class([("X-Example", "foo\r\n bar")])

        # type conversion
        assert headers.get("x-bar", type=int) == 1
        assert headers.getlist("x-bar", type=int) == [1, 2]

        # list like operations
        assert headers[0] == ("Content-Type", "text/plain")
        assert headers[:1] == self.storage_class([("Content-Type", "text/plain")])
        del headers[:2]
        del headers[-1]
        assert headers == self.storage_class([("X-Bar", "1")])

    def test_copying(self):
        a = self.storage_class([("foo", "bar")])
        b = a.copy()
        a.add("foo", "baz")
        assert a.getlist("foo") == ["bar", "baz"]
        assert b.getlist("foo") == ["bar"]

    def test_popping(self):
        headers = self.storage_class([("a", 1)])
        # headers object expect string values. If a non string value
        # is passed, it tries converting it to a string
        assert headers.pop("a") == "1"
        assert headers.pop("b", "2") == "2"

        with pytest.raises(KeyError):
            headers.pop("c")

    def test_set_arguments(self):
        a = self.storage_class()
        a.set("Content-Disposition", "useless")
        a.set("Content-Disposition", "attachment", filename="foo")
        assert a["Content-Disposition"] == "attachment; filename=foo"

    def test_reject_newlines(self):
        h = self.storage_class()

        for variation in "foo\nbar", "foo\r\nbar", "foo\rbar":
            with pytest.raises(ValueError):
                h["foo"] = variation
            with pytest.raises(ValueError):
                h.add("foo", variation)
            with pytest.raises(ValueError):
                h.add("foo", "test", option=variation)
            with pytest.raises(ValueError):
                h.set("foo", variation)
            with pytest.raises(ValueError):
                h.set("foo", "test", option=variation)

    def test_slicing(self):
        # there's nothing wrong with these being native strings
        # Headers doesn't care about the data types
        h = self.storage_class()
        h.set("X-Foo-Poo", "bleh")
        h.set("Content-Type", "application/whocares")
        h.set("X-Forwarded-For", "192.168.0.123")
        h[:] = [(k, v) for k, v in h if k.startswith("X-")]
        assert list(h) == [("X-Foo-Poo", "bleh"), ("X-Forwarded-For", "192.168.0.123")]

    def test_extend(self):
        h = self.storage_class([("a", "0"), ("b", "1"), ("c", "2")])
        h.extend(ds.Headers([("a", "3"), ("a", "4")]))
        assert h.getlist("a") == ["0", "3", "4"]
        h.extend(b=["5", "6"])
        assert h.getlist("b") == ["1", "5", "6"]
        h.extend({"c": "7", "d": ["8", "9"]}, c="10")
        assert h.getlist("c") == ["2", "7", "10"]
        assert h.getlist("d") == ["8", "9"]

        with pytest.raises(TypeError):
            h.extend({"x": "x"}, {"x": "x"})

    def test_update(self):
        h = self.storage_class([("a", "0"), ("b", "1"), ("c", "2")])
        h.update(ds.Headers([("a", "3"), ("a", "4")]))
        assert h.getlist("a") == ["3", "4"]
        h.update(b=["5", "6"])
        assert h.getlist("b") == ["5", "6"]
        h.update({"c": "7", "d": ["8", "9"]})
        assert h.getlist("c") == ["7"]
        assert h.getlist("d") == ["8", "9"]
        h.update({"c": "10"}, c="11")
        assert h.getlist("c") == ["11"]

        with pytest.raises(TypeError):
            h.extend({"x": "x"}, {"x": "x"})

    def test_setlist(self):
        h = self.storage_class([("a", "0"), ("b", "1"), ("c", "2")])
        h.setlist("b", ["3", "4"])
        assert h[1] == ("b", "3")
        assert h[-1] == ("b", "4")
        h.setlist("b", [])
        assert "b" not in h
        h.setlist("d", ["5"])
        assert h["d"] == "5"

    def test_setlistdefault(self):
        h = self.storage_class([("a", "0"), ("b", "1"), ("c", "2")])
        assert h.setlistdefault("a", ["3"]) == ["0"]
        assert h.setlistdefault("d", ["4", "5"]) == ["4", "5"]

    def test_to_wsgi_list(self):
        h = self.storage_class()
        h.set("Key", "Value")
        for key, value in h.to_wsgi_list():
            assert key == "Key"
            assert value == "Value"

    def test_equality(self):
        # test equality, given keys are case insensitive
        h1 = self.storage_class()
        h1.add("X-Foo", "foo")
        h1.add("X-Bar", "bah")
        h1.add("X-Bar", "humbug")

        h2 = self.storage_class()
        h2.add("x-foo", "foo")
        h2.add("x-bar", "bah")
        h2.add("x-bar", "humbug")

        assert h1 == h2

    def test_or(self) -> None:
        a = ds.Headers({"x": 1})
        b = a | {"y": 2}
        assert isinstance(b, ds.Headers)
        assert "x" in b and "y" in b

    def test_ior(self) -> None:
        a = ds.Headers({"x": 1})
        a |= {"y": 2}
        assert "x" in a and "y" in a


class TestEnvironHeaders:
    storage_class = ds.EnvironHeaders

    def test_basic_interface(self):
        # this happens in multiple WSGI servers because they
        # use a vary naive way to convert the headers;
        broken_env = {
            "HTTP_CONTENT_TYPE": "text/html",
            "CONTENT_TYPE": "text/html",
            "HTTP_CONTENT_LENGTH": "0",
            "CONTENT_LENGTH": "0",
            "HTTP_ACCEPT": "*",
            "wsgi.version": (1, 0),
        }
        headers = self.storage_class(broken_env)
        assert headers
        assert len(headers) == 3
        assert sorted(headers) == [
            ("Accept", "*"),
            ("Content-Length", "0"),
            ("Content-Type", "text/html"),
        ]
        assert not self.storage_class({"wsgi.version": (1, 0)})
        assert len(self.storage_class({"wsgi.version": (1, 0)})) == 0
        assert 42 not in headers

    def test_skip_empty_special_vars(self):
        env = {"HTTP_X_FOO": "42", "CONTENT_TYPE": "", "CONTENT_LENGTH": ""}
        headers = self.storage_class(env)
        assert dict(headers) == {"X-Foo": "42"}

        env = {"HTTP_X_FOO": "42", "CONTENT_TYPE": "", "CONTENT_LENGTH": "0"}
        headers = self.storage_class(env)
        assert dict(headers) == {"X-Foo": "42", "Content-Length": "0"}

    def test_return_type_is_str(self):
        headers = self.storage_class({"HTTP_FOO": "\xe2\x9c\x93"})
        assert headers["Foo"] == "\xe2\x9c\x93"
        assert next(iter(headers)) == ("Foo", "\xe2\x9c\x93")

    def test_or(self) -> None:
        headers = ds.EnvironHeaders({"x": "1"})

        with pytest.raises(TypeError):
            headers | {"y": "2"}

    def test_ior(self) -> None:
        headers = ds.EnvironHeaders({})

        with pytest.raises(TypeError):
            headers |= {"y": "2"}

    def test_str(self) -> None:
        headers = ds.EnvironHeaders({"CONTENT_LENGTH": "50", "HTTP_HOST": "test"})
        assert str(headers) == "Content-Length: 50\r\nHost: test\r\n\r\n"


class TestHeaderSet:
    storage_class = ds.HeaderSet

    def test_basic_interface(self):
        hs = self.storage_class()
        hs.add("foo")
        hs.add("bar")
        assert "Bar" in hs
        assert hs.find("foo") == 0
        assert hs.find("BAR") == 1
        assert hs.find("baz") < 0
        hs.discard("missing")
        hs.discard("foo")
        assert hs.find("foo") < 0
        assert hs.find("bar") == 0

        with pytest.raises(IndexError):
            hs.index("missing")

        assert hs.index("bar") == 0
        assert hs
        hs.clear()
        assert not hs


class TestImmutableList:
    storage_class = ds.ImmutableList

    def test_list_hashable(self):
        data = (1, 2, 3, 4)
        store = self.storage_class(data)
        assert hash(data) == hash(store)
        assert data != store


def make_call_asserter(func=None):
    """Utility to assert a certain number of function calls.

    :param func: Additional callback for each function call.

    .. code-block:: python
        assert_calls, func = make_call_asserter()
        with assert_calls(2):
            func()
            func()
    """
    calls = [0]

    @contextmanager
    def asserter(count, msg=None):
        calls[0] = 0
        yield
        assert calls[0] == count

    def wrapped(*args, **kwargs):
        calls[0] += 1
        if func is not None:
            return func(*args, **kwargs)

    return asserter, wrapped


class TestCallbackDict:
    storage_class = ds.CallbackDict

    def test_callback_dict_reads(self):
        assert_calls, func = make_call_asserter()
        initial = {"a": "foo", "b": "bar"}
        dct = self.storage_class(initial=initial, on_update=func)
        with assert_calls(0, "callback triggered by read-only method"):
            # read-only methods
            dct["a"]
            dct.get("a")
            pytest.raises(KeyError, lambda: dct["x"])
            assert "a" in dct
            list(iter(dct))
            dct.copy()
        with assert_calls(0, "callback triggered without modification"):
            # methods that may write but don't
            dct.pop("z", None)
            dct.setdefault("a")

    def test_callback_dict_writes(self):
        assert_calls, func = make_call_asserter()
        initial = {"a": "foo", "b": "bar"}
        dct = self.storage_class(initial=initial, on_update=func)
        with assert_calls(9, "callback not triggered by write method"):
            # always-write methods
            dct["z"] = 123
            dct["z"] = 123  # must trigger again
            del dct["z"]
            dct.pop("b", None)
            dct.setdefault("x")
            dct.popitem()
            dct.update([])
            dct.clear()
            dct |= {}
        with assert_calls(0, "callback triggered by failed del"):
            pytest.raises(KeyError, lambda: dct.__delitem__("x"))
        with assert_calls(0, "callback triggered by failed pop"):
            pytest.raises(KeyError, lambda: dct.pop("x"))


class TestCacheControl:
    def test_repr(self):
        cc = ds.RequestCacheControl([("max-age", "0"), ("private", "True")])
        assert repr(cc) == "<RequestCacheControl max-age='0' private='True'>"

    def test_set_none(self):
        cc = ds.ResponseCacheControl([("max-age", "0")])
        assert cc.no_cache is None
        cc.no_cache = None
        assert cc.no_cache is None
        cc.no_cache = False
        assert cc.no_cache is None

    def test_no_transform(self):
        cc = ds.RequestCacheControl([("no-transform", None)])
        assert cc.no_transform is True
        cc = ds.RequestCacheControl()
        assert cc.no_transform is False

    def test_min_fresh(self):
        cc = ds.RequestCacheControl([("min-fresh", "0")])
        assert cc.min_fresh == 0
        cc = ds.RequestCacheControl([("min-fresh", None)])
        assert cc.min_fresh is None
        cc = ds.RequestCacheControl()
        assert cc.min_fresh is None

    def test_must_understand(self):
        cc = ds.ResponseCacheControl([("must-understand", None)])
        assert cc.must_understand is True
        cc = ds.ResponseCacheControl()
        assert cc.must_understand is False

    def test_stale_while_revalidate(self):
        cc = ds.ResponseCacheControl([("stale-while-revalidate", "1")])
        assert cc.stale_while_revalidate == 1
        cc = ds.ResponseCacheControl()
        assert cc.stale_while_revalidate is None

    def test_stale_if_error(self):
        cc = ds.ResponseCacheControl([("stale-if-error", "1")])
        assert cc.stale_if_error == 1
        cc = ds.ResponseCacheControl()
        assert cc.stale_while_revalidate is None


class TestContentSecurityPolicy:
    def test_construct(self):
        csp = ds.ContentSecurityPolicy([("font-src", "'self'"), ("media-src", "*")])
        assert csp.font_src == "'self'"
        assert csp.media_src == "*"
        policies = [policy.strip() for policy in csp.to_header().split(";")]
        assert "font-src 'self'" in policies
        assert "media-src *" in policies

    def test_properties(self):
        csp = ds.ContentSecurityPolicy()
        csp.default_src = "* 'self' quart.com"
        csp.img_src = "'none'"
        policies = [policy.strip() for policy in csp.to_header().split(";")]
        assert "default-src * 'self' quart.com" in policies
        assert "img-src 'none'" in policies


class TestAccept:
    storage_class = ds.Accept

    def test_accept_basic(self):
        accept = self.storage_class(
            [("tinker", 0), ("tailor", 0.333), ("soldier", 0.667), ("sailor", 1)]
        )
        # check __getitem__ on indices
        assert accept[3] == ("tinker", 0)
        assert accept[2] == ("tailor", 0.333)
        assert accept[1] == ("soldier", 0.667)
        assert accept[0], ("sailor", 1)
        # check __getitem__ on string
        assert accept["tinker"] == 0
        assert accept["tailor"] == 0.333
        assert accept["soldier"] == 0.667
        assert accept["sailor"] == 1
        assert accept["spy"] == 0
        # check quality method
        assert accept.quality("tinker") == 0
        assert accept.quality("tailor") == 0.333
        assert accept.quality("soldier") == 0.667
        assert accept.quality("sailor") == 1
        assert accept.quality("spy") == 0
        # check __contains__
        assert "sailor" in accept
        assert "spy" not in accept
        # check index method
        assert accept.index("tinker") == 3
        assert accept.index("tailor") == 2
        assert accept.index("soldier") == 1
        assert accept.index("sailor") == 0
        with pytest.raises(ValueError):
            accept.index("spy")
        # check find method
        assert accept.find("tinker") == 3
        assert accept.find("tailor") == 2
        assert accept.find("soldier") == 1
        assert accept.find("sailor") == 0
        assert accept.find("spy") == -1
        # check to_header method
        assert accept.to_header() == "sailor,soldier;q=0.667,tailor;q=0.333,tinker;q=0"
        # check best_match method
        assert (
            accept.best_match(["tinker", "tailor", "soldier", "sailor"], default=None)
            == "sailor"
        )
        assert (
            accept.best_match(["tinker", "tailor", "soldier"], default=None)
            == "soldier"
        )
        assert accept.best_match(["tinker", "tailor"], default=None) == "tailor"
        assert accept.best_match(["tinker"], default=None) is None
        assert accept.best_match(["tinker"], default="x") == "x"

    def test_accept_wildcard(self):
        accept = self.storage_class([("*", 0), ("asterisk", 1)])
        assert "*" in accept
        assert accept.best_match(["asterisk", "star"], default=None) == "asterisk"
        assert accept.best_match(["star"], default=None) is None

    def test_accept_keep_order(self):
        accept = self.storage_class([("*", 1)])
        assert accept.best_match(["alice", "bob"]) == "alice"
        assert accept.best_match(["bob", "alice"]) == "bob"
        accept = self.storage_class([("alice", 1), ("bob", 1)])
        assert accept.best_match(["alice", "bob"]) == "alice"
        assert accept.best_match(["bob", "alice"]) == "bob"

    def test_accept_wildcard_specificity(self):
        accept = self.storage_class([("asterisk", 0), ("star", 0.5), ("*", 1)])
        assert accept.best_match(["star", "asterisk"], default=None) == "star"
        assert accept.best_match(["asterisk", "star"], default=None) == "star"
        assert accept.best_match(["asterisk", "times"], default=None) == "times"
        assert accept.best_match(["asterisk"], default=None) is None

    def test_accept_equal_quality(self):
        accept = self.storage_class([("a", 1), ("b", 1)])
        assert accept.best == "a"


class TestMIMEAccept:
    @pytest.mark.parametrize(
        ("values", "matches", "default", "expect"),
        [
            ([("text/*", 1)], ["text/html"], None, "text/html"),
            ([("text/*", 1)], ["image/png"], "text/plain", "text/plain"),
            ([("text/*", 1)], ["image/png"], None, None),
            (
                [("*/*", 1), ("text/html", 1)],
                ["image/png", "text/html"],
                None,
                "text/html",
            ),
            (
                [("*/*", 1), ("text/html", 1)],
                ["image/png", "text/plain"],
                None,
                "image/png",
            ),
            (
                [("*/*", 1), ("text/html", 1), ("image/*", 1)],
                ["image/png", "text/html"],
                None,
                "text/html",
            ),
            (
                [("*/*", 1), ("text/html", 1), ("image/*", 1)],
                ["text/plain", "image/png"],
                None,
                "image/png",
            ),
            (
                [("text/html", 1), ("text/html; level=1", 1)],
                ["text/html;level=1"],
                None,
                "text/html;level=1",
            ),
        ],
    )
    def test_mime_accept(self, values, matches, default, expect):
        accept = ds.MIMEAccept(values)
        match = accept.best_match(matches, default=default)
        assert match == expect


class TestLanguageAccept:
    @pytest.mark.parametrize(
        ("values", "matches", "default", "expect"),
        (
            ([("en-us", 1)], ["en"], None, "en"),
            ([("en", 1)], ["en_US"], None, "en_US"),
            ([("en-GB", 1)], ["en-US"], None, None),
            ([("de_AT", 1), ("de", 0.9)], ["en"], None, None),
            ([("de_AT", 1), ("de", 0.9), ("en-US", 0.8)], ["de", "en"], None, "de"),
            ([("de_AT", 0.9), ("en-US", 1)], ["en"], None, "en"),
            ([("en-us", 1)], ["en-us"], None, "en-us"),
            ([("en-us", 1)], ["en-us", "en"], None, "en-us"),
            ([("en-GB", 1)], ["en-US", "en"], "en-US", "en"),
            ([("de_AT", 1)], ["en-US", "en"], "en-US", "en-US"),
            ([("aus-EN", 1)], ["aus"], None, "aus"),
            ([("aus", 1)], ["aus-EN"], None, "aus-EN"),
        ),
    )
    def test_best_match_fallback(self, values, matches, default, expect):
        accept = ds.LanguageAccept(values)
        best = accept.best_match(matches, default=default)
        assert best == expect


class TestFileStorage:
    storage_class = ds.FileStorage

    def test_mimetype_always_lowercase(self):
        file_storage = self.storage_class(content_type="APPLICATION/JSON")
        assert file_storage.mimetype == "application/json"

    @pytest.mark.parametrize("data", [io.StringIO("one\ntwo"), io.BytesIO(b"one\ntwo")])
    def test_bytes_proper_sentinel(self, data):
        # iterate over new lines and don't enter an infinite loop
        storage = self.storage_class(data)
        idx = -1

        for idx, _line in enumerate(storage):
            assert idx < 2

        assert idx == 1

    @pytest.mark.parametrize("stream", (tempfile.SpooledTemporaryFile, io.BytesIO))
    def test_proxy_can_access_stream_attrs(self, stream):
        """``SpooledTemporaryFile`` doesn't implement some of
        ``IOBase``. Ensure that ``FileStorage`` can still access the
        attributes from the backing file object.

        https://github.com/pallets/werkzeug/issues/1344
        https://github.com/python/cpython/pull/3249
        """
        file_storage = self.storage_class(stream=stream())

        for name in ("fileno", "writable", "readable", "seekable"):
            assert hasattr(file_storage, name)

        file_storage.close()

    def test_save_to_pathlib_dst(self, tmp_path):
        src = tmp_path / "src.txt"
        src.write_text("test")
        dst = tmp_path / "dst.txt"

        with src.open("rb") as f:
            storage = self.storage_class(f)
            storage.save(dst)

        assert dst.read_text() == "test"

    def test_save_to_bytes_io(self):
        storage = self.storage_class(io.BytesIO(b"one\ntwo"))
        dst = io.BytesIO()
        storage.save(dst)
        assert dst.getvalue() == b"one\ntwo"

    def test_save_to_file(self, tmp_path):
        path = tmp_path / "file.data"
        storage = self.storage_class(io.BytesIO(b"one\ntwo"))
        with path.open("wb") as dst:
            storage.save(dst)
        with path.open("rb") as src:
            assert src.read() == b"one\ntwo"


@pytest.mark.parametrize("ranges", ([(0, 1), (-5, None)], [(5, None)]))
def test_range_to_header(ranges):
    header = ds.Range("byes", ranges).to_header()
    r = http.parse_range_header(header)
    assert r.ranges == ranges


@pytest.mark.parametrize(
    "ranges", ([(0, 0)], [(None, 1)], [(1, 0)], [(0, 1), (-5, 10)])
)
def test_range_validates_ranges(ranges):
    with pytest.raises(ValueError):
        ds.Range("bytes", ranges)
