import asyncio
import datetime
import heapq
import itertools
import json
import logging
import os
import pathlib
import pickle
import stat
import sys
import unittest
from http.cookies import BaseCookie, Morsel, SimpleCookie
from operator import not_
from pathlib import Path
from types import MappingProxyType
from unittest import mock

import pytest
from freezegun import freeze_time
from yarl import URL

from aiohttp import CookieJar, DummyCookieJar


def dump_cookiejar() -> bytes:  # pragma: no cover
    """Create pickled data for test_pickle_format()."""
    cj = CookieJar()
    cj.update_cookies(cookies_to_send.__pytest_wrapped__.obj())
    return pickle.dumps(cj._cookies, pickle.HIGHEST_PROTOCOL)


@pytest.fixture
def cookies_to_send():
    return SimpleCookie(
        "shared-cookie=first; "
        "domain-cookie=second; Domain=example.com; "
        "subdomain1-cookie=third; Domain=test1.example.com; "
        "subdomain2-cookie=fourth; Domain=test2.example.com; "
        "dotted-domain-cookie=fifth; Domain=.example.com; "
        "different-domain-cookie=sixth; Domain=different.org; "
        "secure-cookie=seventh; Domain=secure.com; Secure; "
        "no-path-cookie=eighth; Domain=pathtest.com; "
        "path1-cookie=ninth; Domain=pathtest.com; Path=/; "
        "path2-cookie=tenth; Domain=pathtest.com; Path=/one; "
        "path3-cookie=eleventh; Domain=pathtest.com; Path=/one/two; "
        "path4-cookie=twelfth; Domain=pathtest.com; Path=/one/two/; "
        "expires-cookie=thirteenth; Domain=expirestest.com; Path=/;"
        " Expires=Tue, 1 Jan 2999 12:00:00 GMT; "
        "max-age-cookie=fourteenth; Domain=maxagetest.com; Path=/;"
        " Max-Age=60; "
        "invalid-max-age-cookie=fifteenth; Domain=invalid-values.com; "
        " Max-Age=string; "
        "invalid-expires-cookie=sixteenth; Domain=invalid-values.com; "
        " Expires=string;"
    )


@pytest.fixture
def cookies_to_send_with_expired():
    return SimpleCookie(
        "shared-cookie=first; "
        "domain-cookie=second; Domain=example.com; "
        "subdomain1-cookie=third; Domain=test1.example.com; "
        "subdomain2-cookie=fourth; Domain=test2.example.com; "
        "dotted-domain-cookie=fifth; Domain=.example.com; "
        "different-domain-cookie=sixth; Domain=different.org; "
        "secure-cookie=seventh; Domain=secure.com; Secure; "
        "no-path-cookie=eighth; Domain=pathtest.com; "
        "path1-cookie=ninth; Domain=pathtest.com; Path=/; "
        "path2-cookie=tenth; Domain=pathtest.com; Path=/one; "
        "path3-cookie=eleventh; Domain=pathtest.com; Path=/one/two; "
        "path4-cookie=twelfth; Domain=pathtest.com; Path=/one/two/; "
        "expires-cookie=thirteenth; Domain=expirestest.com; Path=/;"
        " Expires=Tue, 1 Jan 1980 12:00:00 GMT; "
        "max-age-cookie=fourteenth; Domain=maxagetest.com; Path=/;"
        " Max-Age=60; "
        "invalid-max-age-cookie=fifteenth; Domain=invalid-values.com; "
        " Max-Age=string; "
        "invalid-expires-cookie=sixteenth; Domain=invalid-values.com; "
        " Expires=string;"
    )


@pytest.fixture
def cookies_to_receive():
    return SimpleCookie(
        "unconstrained-cookie=first; Path=/; "
        "domain-cookie=second; Domain=example.com; Path=/; "
        "subdomain1-cookie=third; Domain=test1.example.com; Path=/; "
        "subdomain2-cookie=fourth; Domain=test2.example.com; Path=/; "
        "dotted-domain-cookie=fifth; Domain=.example.com; Path=/; "
        "different-domain-cookie=sixth; Domain=different.org; Path=/; "
        "no-path-cookie=seventh; Domain=pathtest.com; "
        "path-cookie=eighth; Domain=pathtest.com; Path=/somepath; "
        "wrong-path-cookie=ninth; Domain=pathtest.com; Path=somepath;"
    )


def test_date_parsing() -> None:
    parse_func = CookieJar._parse_date
    utc = datetime.timezone.utc

    assert parse_func("") is None

    # 70 -> 1970
    assert (
        parse_func("Tue, 1 Jan 70 00:00:00 GMT")
        == datetime.datetime(1970, 1, 1, tzinfo=utc).timestamp()
    )

    # 10 -> 2010
    assert (
        parse_func("Tue, 1 Jan 10 00:00:00 GMT")
        == datetime.datetime(2010, 1, 1, tzinfo=utc).timestamp()
    )

    # No day of week string
    assert (
        parse_func("1 Jan 1970 00:00:00 GMT")
        == datetime.datetime(1970, 1, 1, tzinfo=utc).timestamp()
    )

    # No timezone string
    assert (
        parse_func("Tue, 1 Jan 1970 00:00:00")
        == datetime.datetime(1970, 1, 1, tzinfo=utc).timestamp()
    )

    # No year
    assert parse_func("Tue, 1 Jan 00:00:00 GMT") is None

    # No month
    assert parse_func("Tue, 1 1970 00:00:00 GMT") is None

    # No day of month
    assert parse_func("Tue, Jan 1970 00:00:00 GMT") is None

    # No time
    assert parse_func("Tue, 1 Jan 1970 GMT") is None

    # Invalid day of month
    assert parse_func("Tue, 0 Jan 1970 00:00:00 GMT") is None

    # Invalid year
    assert parse_func("Tue, 1 Jan 1500 00:00:00 GMT") is None

    # Invalid time
    assert parse_func("Tue, 1 Jan 1970 77:88:99 GMT") is None


def test_domain_matching() -> None:
    test_func = CookieJar._is_domain_match

    assert test_func("test.com", "test.com")
    assert test_func("test.com", "sub.test.com")

    assert not test_func("test.com", "")
    assert not test_func("test.com", "test.org")
    assert not test_func("diff-test.com", "test.com")
    assert not test_func("test.com", "diff-test.com")
    assert not test_func("test.com", "127.0.0.1")


async def test_constructor(loop, cookies_to_send, cookies_to_receive) -> None:
    jar = CookieJar(loop=loop)
    jar.update_cookies(cookies_to_send)
    jar_cookies = SimpleCookie()
    for cookie in jar:
        dict.__setitem__(jar_cookies, cookie.key, cookie)
    expected_cookies = cookies_to_send
    assert jar_cookies == expected_cookies
    assert jar._loop is loop


async def test_constructor_with_expired(
    loop, cookies_to_send_with_expired, cookies_to_receive
) -> None:
    jar = CookieJar()
    jar.update_cookies(cookies_to_send_with_expired)
    jar_cookies = SimpleCookie()
    for cookie in jar:
        dict.__setitem__(jar_cookies, cookie.key, cookie)
    expected_cookies = cookies_to_send_with_expired
    assert jar_cookies != expected_cookies
    assert jar._loop is loop


def test_save_load(tmp_path, loop, cookies_to_send, cookies_to_receive) -> None:
    file_path = pathlib.Path(str(tmp_path)) / "aiohttp.test.cookie"

    # export cookie jar
    jar_save = CookieJar(loop=loop)
    jar_save.update_cookies(cookies_to_receive)
    jar_save.save(file_path=file_path)

    jar_load = CookieJar(loop=loop)
    jar_load.load(file_path=file_path)

    jar_test = SimpleCookie()
    for cookie in jar_load:
        jar_test[cookie.key] = cookie

    assert jar_test == cookies_to_receive


def test_save_load_partitioned_cookies(tmp_path, loop) -> None:
    file_path = pathlib.Path(str(tmp_path)) / "aiohttp.test2.cookie"
    # export cookie jar
    jar_save = CookieJar(loop=loop)
    jar_save.update_cookies_from_headers(
        ["session=cookie; Partitioned"], URL("https://example.com/")
    )
    jar_save.save(file_path=file_path)
    jar_load = CookieJar(loop=loop)
    jar_load.load(file_path=file_path)
    assert jar_save._cookies == jar_load._cookies


async def test_update_cookie_with_unicode_domain(loop) -> None:
    cookies = (
        "idna-domain-first=first; Domain=xn--9caa.com; Path=/;",
        "idna-domain-second=second; Domain=xn--9caa.com; Path=/;",
    )

    jar = CookieJar(loop=loop)
    jar.update_cookies(SimpleCookie(cookies[0]), URL("http://éé.com/"))
    jar.update_cookies(SimpleCookie(cookies[1]), URL("http://xn--9caa.com/"))

    jar_test = SimpleCookie()
    for cookie in jar:
        jar_test[cookie.key] = cookie

    assert jar_test == SimpleCookie(" ".join(cookies))


async def test_filter_cookie_with_unicode_domain(loop) -> None:
    jar = CookieJar()
    jar.update_cookies(
        SimpleCookie("idna-domain-first=first; Domain=xn--9caa.com; Path=/; ")
    )
    assert len(jar.filter_cookies(URL("http://éé.com"))) == 1
    assert len(jar.filter_cookies(URL("http://xn--9caa.com"))) == 1


@pytest.mark.parametrize(
    ("url", "expected_cookies"),
    (
        (
            "http://pathtest.com/one/two/",
            (
                "no-path-cookie",
                "path1-cookie",
                "path2-cookie",
                "shared-cookie",
                "path3-cookie",
                "path4-cookie",
            ),
        ),
        (
            "http://pathtest.com/one/two",
            (
                "no-path-cookie",
                "path1-cookie",
                "path2-cookie",
                "shared-cookie",
                "path3-cookie",
            ),
        ),
        (
            "http://pathtest.com/one/two/three/",
            (
                "no-path-cookie",
                "path1-cookie",
                "path2-cookie",
                "shared-cookie",
                "path3-cookie",
                "path4-cookie",
            ),
        ),
        (
            "http://test1.example.com/",
            (
                "shared-cookie",
                "domain-cookie",
                "subdomain1-cookie",
                "dotted-domain-cookie",
            ),
        ),
        (
            "http://pathtest.com/",
            (
                "shared-cookie",
                "no-path-cookie",
                "path1-cookie",
            ),
        ),
    ),
)
async def test_filter_cookies_with_domain_path_lookup_multilevelpath(
    loop,
    url,
    expected_cookies,
) -> None:
    jar = CookieJar()
    cookies = SimpleCookie(
        "shared-cookie=first; "
        "domain-cookie=second; Domain=example.com; "
        "subdomain1-cookie=third; Domain=test1.example.com; "
        "subdomain2-cookie=fourth; Domain=test2.example.com; "
        "dotted-domain-cookie=fifth; Domain=.example.com; "
        "different-domain-cookie=sixth; Domain=different.org; "
        "secure-cookie=seventh; Domain=secure.com; Secure; "
        "no-path-cookie=eighth; Domain=pathtest.com; "
        "path1-cookie=ninth; Domain=pathtest.com; Path=/; "
        "path2-cookie=tenth; Domain=pathtest.com; Path=/one; "
        "path3-cookie=eleventh; Domain=pathtest.com; Path=/one/two; "
        "path4-cookie=twelfth; Domain=pathtest.com; Path=/one/two/; "
        "expires-cookie=thirteenth; Domain=expirestest.com; Path=/;"
        " Expires=Tue, 1 Jan 1980 12:00:00 GMT; "
        "max-age-cookie=fourteenth; Domain=maxagetest.com; Path=/;"
        " Max-Age=60; "
        "invalid-max-age-cookie=fifteenth; Domain=invalid-values.com; "
        " Max-Age=string; "
        "invalid-expires-cookie=sixteenth; Domain=invalid-values.com; "
        " Expires=string;"
    )
    jar.update_cookies(cookies)
    cookies = jar.filter_cookies(URL(url))

    assert len(cookies) == len(expected_cookies)
    for c in cookies:
        assert c in expected_cookies


async def test_filter_cookies_str_deprecated(loop: asyncio.AbstractEventLoop) -> None:
    jar = CookieJar()
    jar.update_cookies(SimpleCookie("shared-cookie=first; Domain=example.com;"))
    with pytest.warns(DeprecationWarning):
        jar.filter_cookies("http://éé.com")


async def test_domain_filter_ip_cookie_send() -> None:
    jar = CookieJar()
    cookies = SimpleCookie(
        "shared-cookie=first; "
        "domain-cookie=second; Domain=example.com; "
        "subdomain1-cookie=third; Domain=test1.example.com; "
        "subdomain2-cookie=fourth; Domain=test2.example.com; "
        "dotted-domain-cookie=fifth; Domain=.example.com; "
        "different-domain-cookie=sixth; Domain=different.org; "
        "secure-cookie=seventh; Domain=secure.com; Secure; "
        "no-path-cookie=eighth; Domain=pathtest.com; "
        "path1-cookie=ninth; Domain=pathtest.com; Path=/; "
        "path2-cookie=tenth; Domain=pathtest.com; Path=/one; "
        "path3-cookie=eleventh; Domain=pathtest.com; Path=/one/two; "
        "path4-cookie=twelfth; Domain=pathtest.com; Path=/one/two/; "
        "expires-cookie=thirteenth; Domain=expirestest.com; Path=/;"
        " Expires=Tue, 1 Jan 1980 12:00:00 GMT; "
        "max-age-cookie=fourteenth; Domain=maxagetest.com; Path=/;"
        " Max-Age=60; "
        "invalid-max-age-cookie=fifteenth; Domain=invalid-values.com; "
        " Max-Age=string; "
        "invalid-expires-cookie=sixteenth; Domain=invalid-values.com; "
        " Expires=string;"
    )

    jar.update_cookies(cookies)
    cookies_sent = jar.filter_cookies(URL("http://1.2.3.4/")).output(header="Cookie:")
    assert cookies_sent == "Cookie: shared-cookie=first"


async def test_domain_filter_ip_cookie_receive(cookies_to_receive) -> None:
    jar = CookieJar()

    jar.update_cookies(cookies_to_receive, URL("http://1.2.3.4/"))
    assert len(jar) == 0


@pytest.mark.parametrize(
    ("cookies", "expected", "quote_bool"),
    [
        (
            "shared-cookie=first; ip-cookie=second; Domain=127.0.0.1;",
            "Cookie: ip-cookie=second\r\nCookie: shared-cookie=first",
            True,
        ),
        ('ip-cookie="second"; Domain=127.0.0.1;', 'Cookie: ip-cookie="second"', True),
        ("custom-cookie=value/one;", 'Cookie: custom-cookie="value/one"', True),
        ("custom-cookie=value1;", "Cookie: custom-cookie=value1", True),
        ("custom-cookie=value/one;", "Cookie: custom-cookie=value/one", False),
        ('foo="quoted_value"', 'Cookie: foo="quoted_value"', True),
        ('foo="quoted_value"; domain=127.0.0.1', 'Cookie: foo="quoted_value"', True),
    ],
    ids=(
        "IP domain preserved",
        "no shared cookie",
        "quoted cookie with special char",
        "quoted cookie w/o special char",
        "unquoted cookie with special char",
        "pre-quoted cookie",
        "pre-quoted cookie with domain",
    ),
)
async def test_quotes_correctly_based_on_input(
    loop, cookies, expected, quote_bool
) -> None:
    jar = CookieJar(unsafe=True, quote_cookie=quote_bool)
    jar.update_cookies(SimpleCookie(cookies))
    cookies_sent = jar.filter_cookies(URL("http://127.0.0.1/")).output(header="Cookie:")
    assert cookies_sent == expected


async def test_ignore_domain_ending_with_dot(loop) -> None:
    jar = CookieJar(loop=loop, unsafe=True)
    jar.update_cookies(
        SimpleCookie("cookie=val; Domain=example.com.;"), URL("http://www.example.com")
    )
    cookies_sent = jar.filter_cookies(URL("http://www.example.com/"))
    assert cookies_sent.output(header="Cookie:") == "Cookie: cookie=val"
    cookies_sent = jar.filter_cookies(URL("http://example.com/"))
    assert cookies_sent.output(header="Cookie:") == ""


class TestCookieJarBase(unittest.TestCase):
    def setUp(self):
        self.loop = asyncio.new_event_loop()
        asyncio.set_event_loop(None)

        # N.B. those need to be overridden in child test cases
        async def make_jar():
            return CookieJar()

        self.jar = self.loop.run_until_complete(make_jar())

    def tearDown(self):
        self.loop.close()

    def request_reply_with_same_url(self, url):
        self.jar.update_cookies(self.cookies_to_send)
        cookies_sent = self.jar.filter_cookies(URL(url))

        self.jar.clear()

        self.jar.update_cookies(self.cookies_to_receive, URL(url))
        cookies_received = SimpleCookie()
        for cookie in self.jar:
            dict.__setitem__(cookies_received, cookie.key, cookie)

        self.jar.clear()

        return cookies_sent, cookies_received


class TestCookieJarSafe(TestCookieJarBase):
    def setUp(self):
        super().setUp()

        self.cookies_to_send = SimpleCookie(
            "shared-cookie=first; "
            "domain-cookie=second; Domain=example.com; "
            "subdomain1-cookie=third; Domain=test1.example.com; "
            "subdomain2-cookie=fourth; Domain=test2.example.com; "
            "dotted-domain-cookie=fifth; Domain=.example.com; "
            "different-domain-cookie=sixth; Domain=different.org; "
            "secure-cookie=seventh; Domain=secure.com; Secure; "
            "no-path-cookie=eighth; Domain=pathtest.com; "
            "path1-cookie=ninth; Domain=pathtest.com; Path=/; "
            "path2-cookie=tenth; Domain=pathtest.com; Path=/one; "
            "path3-cookie=eleventh; Domain=pathtest.com; Path=/one/two; "
            "path4-cookie=twelfth; Domain=pathtest.com; Path=/one/two/; "
            "expires-cookie=thirteenth; Domain=expirestest.com; Path=/;"
            " Expires=Tue, 1 Jan 1980 12:00:00 GMT; "
            "max-age-cookie=fourteenth; Domain=maxagetest.com; Path=/;"
            " Max-Age=60; "
            "invalid-max-age-cookie=fifteenth; Domain=invalid-values.com; "
            " Max-Age=string; "
            "invalid-expires-cookie=sixteenth; Domain=invalid-values.com; "
            " Expires=string;"
        )

        self.cookies_to_receive = SimpleCookie(
            "unconstrained-cookie=first; Path=/; "
            "domain-cookie=second; Domain=example.com; Path=/; "
            "subdomain1-cookie=third; Domain=test1.example.com; Path=/; "
            "subdomain2-cookie=fourth; Domain=test2.example.com; Path=/; "
            "dotted-domain-cookie=fifth; Domain=.example.com; Path=/; "
            "different-domain-cookie=sixth; Domain=different.org; Path=/; "
            "no-path-cookie=seventh; Domain=pathtest.com; "
            "path-cookie=eighth; Domain=pathtest.com; Path=/somepath; "
            "wrong-path-cookie=ninth; Domain=pathtest.com; Path=somepath;"
        )

        async def make_jar():
            return CookieJar()

        self.jar = self.loop.run_until_complete(make_jar())

    def timed_request(self, url, update_time, send_time):
        if isinstance(update_time, int):
            update_time = datetime.timedelta(seconds=update_time)
        elif isinstance(update_time, float):
            update_time = datetime.datetime.fromtimestamp(update_time)
        if isinstance(send_time, int):
            send_time = datetime.timedelta(seconds=send_time)
        elif isinstance(send_time, float):
            send_time = datetime.datetime.fromtimestamp(send_time)

        with freeze_time(update_time):
            self.jar.update_cookies(self.cookies_to_send)

        with freeze_time(send_time):
            cookies_sent = self.jar.filter_cookies(URL(url))

        self.jar.clear()

        return cookies_sent

    def test_domain_filter_same_host(self) -> None:
        cookies_sent, cookies_received = self.request_reply_with_same_url(
            "http://example.com/"
        )

        self.assertEqual(
            set(cookies_sent.keys()),
            {"shared-cookie", "domain-cookie", "dotted-domain-cookie"},
        )

        self.assertEqual(
            set(cookies_received.keys()),
            {"unconstrained-cookie", "domain-cookie", "dotted-domain-cookie"},
        )

    def test_domain_filter_same_host_and_subdomain(self) -> None:
        cookies_sent, cookies_received = self.request_reply_with_same_url(
            "http://test1.example.com/"
        )

        self.assertEqual(
            set(cookies_sent.keys()),
            {
                "shared-cookie",
                "domain-cookie",
                "subdomain1-cookie",
                "dotted-domain-cookie",
            },
        )

        self.assertEqual(
            set(cookies_received.keys()),
            {
                "unconstrained-cookie",
                "domain-cookie",
                "subdomain1-cookie",
                "dotted-domain-cookie",
            },
        )

    def test_domain_filter_same_host_diff_subdomain(self) -> None:
        cookies_sent, cookies_received = self.request_reply_with_same_url(
            "http://different.example.com/"
        )

        self.assertEqual(
            set(cookies_sent.keys()),
            {"shared-cookie", "domain-cookie", "dotted-domain-cookie"},
        )

        self.assertEqual(
            set(cookies_received.keys()),
            {"unconstrained-cookie", "domain-cookie", "dotted-domain-cookie"},
        )

    def test_domain_filter_diff_host(self) -> None:
        cookies_sent, cookies_received = self.request_reply_with_same_url(
            "http://different.org/"
        )

        self.assertEqual(
            set(cookies_sent.keys()), {"shared-cookie", "different-domain-cookie"}
        )

        self.assertEqual(
            set(cookies_received.keys()),
            {"unconstrained-cookie", "different-domain-cookie"},
        )

    def test_domain_filter_host_only(self) -> None:
        self.jar.update_cookies(self.cookies_to_receive, URL("http://example.com/"))
        sub_cookie = SimpleCookie("subdomain=spam; Path=/;")
        self.jar.update_cookies(sub_cookie, URL("http://foo.example.com/"))

        cookies_sent = self.jar.filter_cookies(URL("http://foo.example.com/"))
        self.assertIn("subdomain", set(cookies_sent.keys()))
        self.assertNotIn("unconstrained-cookie", set(cookies_sent.keys()))

    def test_secure_filter(self) -> None:
        cookies_sent, _ = self.request_reply_with_same_url("http://secure.com/")

        self.assertEqual(set(cookies_sent.keys()), {"shared-cookie"})

        cookies_sent, _ = self.request_reply_with_same_url("https://secure.com/")

        self.assertEqual(set(cookies_sent.keys()), {"shared-cookie", "secure-cookie"})

    def test_path_filter_root(self) -> None:
        cookies_sent, _ = self.request_reply_with_same_url("http://pathtest.com/")

        self.assertEqual(
            set(cookies_sent.keys()),
            {"shared-cookie", "no-path-cookie", "path1-cookie"},
        )

    def test_path_filter_folder(self) -> None:

        cookies_sent, _ = self.request_reply_with_same_url("http://pathtest.com/one/")

        self.assertEqual(
            set(cookies_sent.keys()),
            {"shared-cookie", "no-path-cookie", "path1-cookie", "path2-cookie"},
        )

    def test_path_filter_file(self) -> None:

        cookies_sent, _ = self.request_reply_with_same_url(
            "http://pathtest.com/one/two"
        )

        self.assertEqual(
            set(cookies_sent.keys()),
            {
                "shared-cookie",
                "no-path-cookie",
                "path1-cookie",
                "path2-cookie",
                "path3-cookie",
            },
        )

    def test_path_filter_subfolder(self) -> None:

        cookies_sent, _ = self.request_reply_with_same_url(
            "http://pathtest.com/one/two/"
        )

        self.assertEqual(
            set(cookies_sent.keys()),
            {
                "shared-cookie",
                "no-path-cookie",
                "path1-cookie",
                "path2-cookie",
                "path3-cookie",
                "path4-cookie",
            },
        )

    def test_path_filter_subsubfolder(self) -> None:

        cookies_sent, _ = self.request_reply_with_same_url(
            "http://pathtest.com/one/two/three/"
        )

        self.assertEqual(
            set(cookies_sent.keys()),
            {
                "shared-cookie",
                "no-path-cookie",
                "path1-cookie",
                "path2-cookie",
                "path3-cookie",
                "path4-cookie",
            },
        )

    def test_path_filter_different_folder(self) -> None:

        cookies_sent, _ = self.request_reply_with_same_url(
            "http://pathtest.com/hundred/"
        )

        self.assertEqual(
            set(cookies_sent.keys()),
            {"shared-cookie", "no-path-cookie", "path1-cookie"},
        )

    def test_path_value(self) -> None:
        _, cookies_received = self.request_reply_with_same_url("http://pathtest.com/")

        self.assertEqual(
            set(cookies_received.keys()),
            {
                "unconstrained-cookie",
                "no-path-cookie",
                "path-cookie",
                "wrong-path-cookie",
            },
        )

        self.assertEqual(cookies_received["no-path-cookie"]["path"], "/")
        self.assertEqual(cookies_received["path-cookie"]["path"], "/somepath")
        self.assertEqual(cookies_received["wrong-path-cookie"]["path"], "/")

    def test_expires(self) -> None:
        ts_before = datetime.datetime(
            1975, 1, 1, tzinfo=datetime.timezone.utc
        ).timestamp()

        ts_after = datetime.datetime(
            2030, 1, 1, tzinfo=datetime.timezone.utc
        ).timestamp()

        cookies_sent = self.timed_request(
            "http://expirestest.com/", ts_before, ts_before
        )

        self.assertEqual(set(cookies_sent.keys()), {"shared-cookie", "expires-cookie"})

        cookies_sent = self.timed_request(
            "http://expirestest.com/", ts_before, ts_after
        )

        self.assertEqual(set(cookies_sent.keys()), {"shared-cookie"})

    def test_max_age(self) -> None:
        cookies_sent = self.timed_request("http://maxagetest.com/", 1000, 1000)

        self.assertEqual(set(cookies_sent.keys()), {"shared-cookie", "max-age-cookie"})

        cookies_sent = self.timed_request("http://maxagetest.com/", 1000, 2000)

        self.assertEqual(set(cookies_sent.keys()), {"shared-cookie"})

    def test_invalid_values(self) -> None:
        cookies_sent, cookies_received = self.request_reply_with_same_url(
            "http://invalid-values.com/"
        )

        self.assertEqual(
            set(cookies_sent.keys()),
            {"shared-cookie", "invalid-max-age-cookie", "invalid-expires-cookie"},
        )

        cookie = cookies_sent["invalid-max-age-cookie"]
        self.assertEqual(cookie["max-age"], "")

        cookie = cookies_sent["invalid-expires-cookie"]
        self.assertEqual(cookie["expires"], "")

    def test_cookie_not_expired_when_added_after_removal(self) -> None:
        # Test case for https://github.com/aio-libs/aiohttp/issues/2084
        timestamps = [
            533588.993,
            533588.993,
            533588.993,
            533588.993,
            533589.093,
            533589.093,
        ]

        loop = mock.Mock()
        loop.time.side_effect = itertools.chain(
            timestamps, itertools.cycle([timestamps[-1]])
        )

        async def make_jar():
            return CookieJar(unsafe=True)

        jar = self.loop.run_until_complete(make_jar())
        # Remove `foo` cookie.
        jar.update_cookies(SimpleCookie('foo=""; Max-Age=0'))
        # Set `foo` cookie to `bar`.
        jar.update_cookies(SimpleCookie('foo="bar"'))

        # Assert that there is a cookie.
        assert len(jar) == 1

    def test_path_filter_diff_folder_same_name(self) -> None:
        async def make_jar():
            return CookieJar(unsafe=True)

        jar = self.loop.run_until_complete(make_jar())

        jar.update_cookies(
            SimpleCookie("path-cookie=zero; Domain=pathtest.com; Path=/; ")
        )
        jar.update_cookies(
            SimpleCookie("path-cookie=one; Domain=pathtest.com; Path=/one; ")
        )
        self.assertEqual(len(jar), 2)

        jar_filtered = jar.filter_cookies(URL("http://pathtest.com/"))
        self.assertEqual(len(jar_filtered), 1)
        self.assertEqual(jar_filtered["path-cookie"].value, "zero")

        jar_filtered = jar.filter_cookies(URL("http://pathtest.com/one"))
        self.assertEqual(len(jar_filtered), 1)
        self.assertEqual(jar_filtered["path-cookie"].value, "one")

    def test_path_filter_diff_folder_same_name_return_best_match_independent_from_put_order(
        self,
    ) -> None:
        async def make_jar():
            return CookieJar(unsafe=True)

        jar = self.loop.run_until_complete(make_jar())
        jar.update_cookies(
            SimpleCookie("path-cookie=one; Domain=pathtest.com; Path=/one; ")
        )
        jar.update_cookies(
            SimpleCookie("path-cookie=zero; Domain=pathtest.com; Path=/; ")
        )
        jar.update_cookies(
            SimpleCookie("path-cookie=two; Domain=pathtest.com; Path=/second; ")
        )
        self.assertEqual(len(jar), 3)

        jar_filtered = jar.filter_cookies(URL("http://pathtest.com/"))
        self.assertEqual(len(jar_filtered), 1)
        self.assertEqual(jar_filtered["path-cookie"].value, "zero")

        jar_filtered = jar.filter_cookies(URL("http://pathtest.com/second"))
        self.assertEqual(len(jar_filtered), 1)
        self.assertEqual(jar_filtered["path-cookie"].value, "two")

        jar_filtered = jar.filter_cookies(URL("http://pathtest.com/one"))
        self.assertEqual(len(jar_filtered), 1)
        self.assertEqual(jar_filtered["path-cookie"].value, "one")


async def test_dummy_cookie_jar() -> None:
    cookie = SimpleCookie("foo=bar; Domain=example.com;")
    dummy_jar = DummyCookieJar()
    assert dummy_jar.unsafe is False
    assert dummy_jar.quote_cookie is True
    assert len(dummy_jar) == 0
    dummy_jar.update_cookies(cookie)
    assert len(dummy_jar) == 0
    with pytest.raises(StopIteration):
        next(iter(dummy_jar))
    assert not dummy_jar.filter_cookies(URL("http://example.com/"))
    dummy_jar.clear()


async def test_dummy_cookie_jar_cookies_property() -> None:
    dummy_jar = DummyCookieJar()
    assert dict(dummy_jar.cookies) == {}
    assert dummy_jar.host_only_cookies == frozenset()


async def test_cookie_jar_cookies_property() -> None:
    jar = CookieJar()
    cookie = SimpleCookie(
        "shared-cookie=first; domain-cookie=second; Domain=example.com; Path=/; "
    )
    jar.update_cookies(cookie, URL("http://example.com/"))

    cookies = jar.cookies
    # Should be a read-only view
    assert isinstance(cookies, MappingProxyType)
    # Should contain the stored cookies with their full attributes
    found_names = {name for simple_cookie in cookies.values() for name in simple_cookie}
    assert "shared-cookie" in found_names
    assert "domain-cookie" in found_names
    # Verify that domain attribute is preserved
    for key, simple_cookie in cookies.items():
        for name, morsel in simple_cookie.items():
            if name == "domain-cookie":
                assert morsel["domain"] == "example.com"
                assert morsel["path"] == "/"


async def test_cookie_jar_host_only_cookies_property() -> None:
    jar = CookieJar()
    # Cookies without an explicit Domain attribute are host-only
    cookie = SimpleCookie("hostonly=value;")
    jar.update_cookies(cookie, URL("http://example.com/"))

    host_only = jar.host_only_cookies
    assert isinstance(host_only, frozenset)
    assert ("example.com", "hostonly") in host_only


async def test_cookie_jar_cookies_property_immutable() -> None:
    jar = CookieJar()
    cookie = SimpleCookie("foo=bar;")
    jar.update_cookies(cookie, URL("http://example.com/"))
    cookies = jar.cookies
    with pytest.raises(TypeError):
        cookies[("new", "key")] = SimpleCookie()  # type: ignore[index]


async def test_loose_cookies_types() -> None:
    jar = CookieJar()

    accepted_types = [
        [("str", BaseCookie())],
        [("str", Morsel())],
        [
            ("str", "str"),
        ],
        {"str": BaseCookie()},
        {"str": Morsel()},
        {"str": "str"},
        SimpleCookie(),
    ]

    for loose_cookies_type in accepted_types:
        jar.update_cookies(cookies=loose_cookies_type)


async def test_cookie_jar_clear_all():
    sut = CookieJar()
    cookie = SimpleCookie()
    cookie["foo"] = "bar"
    sut.update_cookies(cookie)

    sut.clear()
    assert len(sut) == 0


async def test_cookie_jar_clear_expired():
    sut = CookieJar()

    cookie = SimpleCookie()

    cookie["foo"] = "bar"
    cookie["foo"]["expires"] = "Tue, 1 Jan 1990 12:00:00 GMT"

    with freeze_time("1980-01-01"):
        sut.update_cookies(cookie)

    for _ in range(2):
        sut.clear(not_)
        with freeze_time("1980-01-01"):
            assert len(sut) == 0


async def test_cookie_jar_expired_changes() -> None:
    """Test that expire time changes are handled as expected."""
    jar = CookieJar()

    cookie_eleven_am = SimpleCookie()
    cookie_eleven_am["foo"] = "bar"
    cookie_eleven_am["foo"]["expires"] = "Tue, 1 Jan 1990 11:00:00 GMT"

    cookie_noon = SimpleCookie()
    cookie_noon["foo"] = "bar"
    cookie_noon["foo"]["expires"] = "Tue, 1 Jan 1990 12:00:00 GMT"

    cookie_one_pm = SimpleCookie()
    cookie_one_pm["foo"] = "bar"
    cookie_one_pm["foo"]["expires"] = "Tue, 1 Jan 1990 13:00:00 GMT"

    cookie_two_pm = SimpleCookie()
    cookie_two_pm["foo"] = "bar"
    cookie_two_pm["foo"]["expires"] = "Tue, 1 Jan 1990 14:00:00 GMT"

    with freeze_time() as freezer:
        freezer.move_to("1990-01-01 10:00:00+00:00")
        jar.update_cookies(cookie_noon)
        assert len(jar) == 1
        matched_cookies = jar.filter_cookies(URL("/"))
        assert len(matched_cookies) == 1
        assert "foo" in matched_cookies

        jar.update_cookies(cookie_eleven_am)
        matched_cookies = jar.filter_cookies(URL("/"))
        assert len(matched_cookies) == 1
        assert "foo" in matched_cookies

        jar.update_cookies(cookie_one_pm)
        matched_cookies = jar.filter_cookies(URL("/"))
        assert len(matched_cookies) == 1
        assert "foo" in matched_cookies

        jar.update_cookies(cookie_two_pm)
        matched_cookies = jar.filter_cookies(URL("/"))
        assert len(matched_cookies) == 1
        assert "foo" in matched_cookies

        freezer.move_to("1990-01-01 13:00:00+00:00")
        matched_cookies = jar.filter_cookies(URL("/"))
        assert len(matched_cookies) == 1
        assert "foo" in matched_cookies

        freezer.move_to("1990-01-01 14:00:00+00:00")
        matched_cookies = jar.filter_cookies(URL("/"))
        assert len(matched_cookies) == 0


async def test_cookie_jar_duplicates_with_expire_heap() -> None:
    """Test that duplicate cookies do not grow the expires heap."""
    jar = CookieJar()

    cookie_eleven_am = SimpleCookie()
    cookie_eleven_am["foo"] = "bar"
    cookie_eleven_am["foo"]["expires"] = "Tue, 1 Jan 1990 11:00:00 GMT"

    cookie_two_pm = SimpleCookie()
    cookie_two_pm["foo"] = "bar"
    cookie_two_pm["foo"]["expires"] = "Tue, 1 Jan 1990 14:00:00 GMT"

    with freeze_time() as freezer:
        freezer.move_to("1990-01-01 10:00:00+00:00")

        for _ in range(10):
            jar.update_cookies(cookie_eleven_am)

        assert len(jar) == 1
        matched_cookies = jar.filter_cookies(URL("/"))
        assert len(matched_cookies) == 1
        assert "foo" in matched_cookies

        assert len(jar._expire_heap) == 1

        freezer.move_to("1990-01-01 16:00:00+00:00")
        jar.update_cookies(cookie_two_pm)
        matched_cookies = jar.filter_cookies(URL("/"))
        assert len(matched_cookies) == 0
        assert len(jar._expire_heap) == 0


async def test_cookie_jar_filter_cookies_expires() -> None:
    """Test that calling filter_cookies will expire stale cookies."""
    jar = CookieJar()
    assert len(jar) == 0

    cookie = SimpleCookie()

    cookie["foo"] = "bar"
    cookie["foo"]["expires"] = "Tue, 1 Jan 1990 12:00:00 GMT"

    with freeze_time("1980-01-01"):
        jar.update_cookies(cookie)

    assert len(jar) == 1

    # filter_cookies should expire stale cookies
    jar.filter_cookies(URL("http://any.com/"))

    assert len(jar) == 0


async def test_cookie_jar_heap_cleanup() -> None:
    """Test that the heap gets cleaned up when there are many old expirations."""
    jar = CookieJar()
    # The heap should not be cleaned up when there are less than 100 expiration changes
    min_cookies_to_cleanup = 100

    with freeze_time() as freezer:
        freezer.move_to("1990-01-01 09:00:00+00:00")

        start_time = datetime.datetime(
            1990, 1, 1, 10, 0, 0, tzinfo=datetime.timezone.utc
        )
        for i in range(min_cookies_to_cleanup):
            cookie = SimpleCookie()
            cookie["foo"] = "bar"
            cookie["foo"]["expires"] = (
                start_time + datetime.timedelta(seconds=i)
            ).strftime("%a, %d %b %Y %H:%M:%S GMT")
            jar.update_cookies(cookie)
            assert len(jar._expire_heap) == i + 1

        assert len(jar._expire_heap) == min_cookies_to_cleanup

        # Now that we reached the minimum number of cookies to cleanup,
        # add one more cookie to trigger the cleanup
        cookie = SimpleCookie()
        cookie["foo"] = "bar"
        cookie["foo"]["expires"] = (
            start_time + datetime.timedelta(seconds=i + 1)
        ).strftime("%a, %d %b %Y %H:%M:%S GMT")
        jar.update_cookies(cookie)

        # Verify that the heap has been cleaned up
        assert len(jar) == 1
        matched_cookies = jar.filter_cookies(URL("/"))
        assert len(matched_cookies) == 1
        assert "foo" in matched_cookies
        # The heap should have been cleaned up
        assert len(jar._expire_heap) == 1


async def test_cookie_jar_heap_maintains_order_after_cleanup() -> None:
    """Test that order is maintained after cleanup."""
    jar = CookieJar()
    # The heap should not be cleaned up when there are less than 100 expiration changes
    min_cookies_to_cleanup = 100

    with freeze_time() as freezer:
        freezer.move_to("1990-01-01 09:00:00+00:00")

        for hour in (12, 13):
            for i in range(min_cookies_to_cleanup):
                cookie = SimpleCookie()
                cookie["foo"] = "bar"
                cookie["foo"]["domain"] = f"example{i}.com"
                cookie["foo"]["expires"] = f"Tue, 1 Jan 1990 {hour}:00:00 GMT"
                jar.update_cookies(cookie)

        # Get the jar into a state where the next cookie will trigger the cleanup
        assert len(jar._expire_heap) == min_cookies_to_cleanup * 2
        assert len(jar._expirations) == min_cookies_to_cleanup

        cookie = SimpleCookie()
        cookie["foo"] = "bar"
        cookie["foo"]["domain"] = "example0.com"
        cookie["foo"]["expires"] = "Tue, 1 Jan 1990 14:00:00 GMT"
        jar.update_cookies(cookie)

        assert len(jar) == 100
        # The heap should have been cleaned up
        assert len(jar._expire_heap) == 100

        # Verify that the heap is still ordered
        heap_before = jar._expire_heap.copy()
        heapq.heapify(jar._expire_heap)
        assert heap_before == jar._expire_heap


async def test_cookie_jar_clear_domain() -> None:
    sut = CookieJar()
    cookie = SimpleCookie()
    cookie["foo"] = "bar"
    cookie["domain_cookie"] = "value"
    cookie["domain_cookie"]["domain"] = "example.com"
    cookie["subdomain_cookie"] = "value"
    cookie["subdomain_cookie"]["domain"] = "test.example.com"
    sut.update_cookies(cookie)

    sut.clear_domain("example.com")
    iterator = iter(sut)
    morsel = next(iterator)
    assert morsel.key == "foo"
    assert morsel.value == "bar"
    with pytest.raises(StopIteration):
        next(iterator)


async def test_pickle_format(cookies_to_send) -> None:
    """Test if cookiejar pickle format breaks.

    If this test fails, it may indicate that saved cookiejars will stop working.
    If that happens then:
        1. Avoid releasing the change in a bugfix release.
        2. Try to include a migration script in the release notes (example below).
        3. Use dump_cookiejar() at the top of this file to update `pickled`.

    Depending on the changes made, a migration script might look like:
        import pickle
        with file_path.open("rb") as f:
            cookies = pickle.load(f)

        morsels = [(name, m) for c in cookies.values() for name, m in c.items()]
        cookies.clear()
        for name, m in morsels:
            cookies[(m["domain"], m["path"])][name] = m

        with file_path.open("wb") as f:
            pickle.dump(cookies, f, pickle.HIGHEST_PROTOCOL)
    """
    if sys.version_info < (3, 14):
        pickled = b"\x80\x04\x95\xc8\x0b\x00\x00\x00\x00\x00\x00\x8c\x0bcollections\x94\x8c\x0bdefaultdict\x94\x93\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94\x85\x94R\x94(\x8c\x00\x94h\x08\x86\x94h\x05)\x81\x94\x8c\rshared-cookie\x94h\x03\x8c\x06Morsel\x94\x93\x94)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94\x8c\x01/\x94\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\x08\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(\x8c\x03key\x94h\x0b\x8c\x05value\x94\x8c\x05first\x94\x8c\x0bcoded_value\x94h\x1cubs\x8c\x0bexample.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\rdomain-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\x1e\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah!h\x1b\x8c\x06second\x94h\x1dh-ub\x8c\x14dotted-domain-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94\x8c\x0bexample.com\x94\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah.h\x1b\x8c\x05fifth\x94h\x1dh;ubu\x8c\x11test1.example.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x11subdomain1-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h<\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah?h\x1b\x8c\x05third\x94h\x1dhKubs\x8c\x11test2.example.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x11subdomain2-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94hL\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ahOh\x1b\x8c\x06fourth\x94h\x1dh[ubs\x8c\rdifferent.org\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x17different-domain-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\\\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah_h\x1b\x8c\x05sixth\x94h\x1dhkubs\x8c\nsecure.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\rsecure-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94hl\x8c\x07max-age\x94h\x08\x8c\x06secure\x94\x88\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ahoh\x1b\x8c\x07seventh\x94h\x1dh{ubs\x8c\x0cpathtest.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\x0eno-path-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h|\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\x7fh\x1b\x8c\x06eighth\x94h\x1dh\x8bub\x8c\x0cpath1-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94\x8c\x0cpathtest.com\x94\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\x8ch\x1b\x8c\x05ninth\x94h\x1dh\x99ubu\x8c\x0cpathtest.com\x94\x8c\x04/one\x94\x86\x94h\x05)\x81\x94\x8c\x0cpath2-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x9b\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\x9a\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\x9eh\x1b\x8c\x05tenth\x94h\x1dh\xaaubs\x8c\x0cpathtest.com\x94\x8c\x08/one/two\x94\x86\x94h\x05)\x81\x94(\x8c\x0cpath3-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\xac\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\xab\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xafh\x1b\x8c\x08eleventh\x94h\x1dh\xbbub\x8c\x0cpath4-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94\x8c\t/one/two/\x94\x8c\x07comment\x94h\x08\x8c\x06domain\x94\x8c\x0cpathtest.com\x94\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xbch\x1b\x8c\x07twelfth\x94h\x1dh\xcaubu\x8c\x0fexpirestest.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x0eexpires-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94\x8c\x1cTue, 1 Jan 2999 12:00:00 GMT\x94\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\xcb\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xceh\x1b\x8c\nthirteenth\x94h\x1dh\xdbubs\x8c\x0emaxagetest.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x0emax-age-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\xdc\x8c\x07max-age\x94\x8c\x0260\x94\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xdfh\x1b\x8c\nfourteenth\x94h\x1dh\xecubs\x8c\x12invalid-values.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\x16invalid-max-age-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\xed\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xf0h\x1b\x8c\tfifteenth\x94h\x1dh\xfcub\x8c\x16invalid-expires-cookie\x94h\r)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94h\x11\x8c\x07comment\x94h\x08\x8c\x06domain\x94\x8c\x12invalid-values.com\x94\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08u}\x94(h\x1ah\xfdh\x1b\x8c\tsixteenth\x94h\x1dj\n\x01\x00\x00ubuu."
    else:
        pickled = b'\x80\x05\x95\x06\x08\x00\x00\x00\x00\x00\x00\x8c\x0bcollections\x94\x8c\x0bdefaultdict\x94\x93\x94\x8c\x0chttp.cookies\x94\x8c\x0cSimpleCookie\x94\x93\x94\x85\x94R\x94(\x8c\x00\x94h\x08\x86\x94h\x05)\x81\x94\x8c\rshared-cookie\x94h\x03\x8c\x06Morsel\x94\x93\x94)\x81\x94(\x8c\x07expires\x94h\x08\x8c\x04path\x94\x8c\x01/\x94\x8c\x07comment\x94h\x08\x8c\x06domain\x94h\x08\x8c\x07max-age\x94h\x08\x8c\x06secure\x94h\x08\x8c\x08httponly\x94h\x08\x8c\x07version\x94h\x08\x8c\x08samesite\x94h\x08\x8c\x0bpartitioned\x94h\x08u}\x94(\x8c\x03key\x94h\x0b\x8c\x05value\x94\x8c\x05first\x94\x8c\x0bcoded_value\x94h\x1dubs\x8c\x0bexample.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\rdomain-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13h\x1fh\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh"h\x1c\x8c\x06second\x94h\x1eh%ub\x8c\x14dotted-domain-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13\x8c\x0bexample.com\x94h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh&h\x1c\x8c\x05fifth\x94h\x1eh*ubu\x8c\x11test1.example.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x11subdomain1-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13h+h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh.h\x1c\x8c\x05third\x94h\x1eh1ubs\x8c\x11test2.example.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x11subdomain2-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13h2h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh5h\x1c\x8c\x06fourth\x94h\x1eh8ubs\x8c\rdifferent.org\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x17different-domain-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13h9h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh<h\x1c\x8c\x05sixth\x94h\x1eh?ubs\x8c\nsecure.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\rsecure-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13h@h\x14h\x08h\x15\x88h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhCh\x1c\x8c\x07seventh\x94h\x1ehFubs\x8c\x0cpathtest.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\x0eno-path-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13hGh\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhJh\x1c\x8c\x06eighth\x94h\x1ehMub\x8c\x0cpath1-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13\x8c\x0cpathtest.com\x94h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhNh\x1c\x8c\x05ninth\x94h\x1ehRubu\x8c\x0cpathtest.com\x94\x8c\x04/one\x94\x86\x94h\x05)\x81\x94\x8c\x0cpath2-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10hTh\x12h\x08h\x13hSh\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhWh\x1c\x8c\x05tenth\x94h\x1ehZubs\x8c\x0cpathtest.com\x94\x8c\x08/one/two\x94\x86\x94h\x05)\x81\x94(\x8c\x0cpath3-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\\h\x12h\x08h\x13h[h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh_h\x1c\x8c\x08eleventh\x94h\x1ehbub\x8c\x0cpath4-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10\x8c\t/one/two/\x94h\x12h\x08h\x13\x8c\x0cpathtest.com\x94h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhch\x1c\x8c\x07twelfth\x94h\x1ehhubu\x8c\x0fexpirestest.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x0eexpires-cookie\x94h\r)\x81\x94(h\x0f\x8c\x1cTue, 1 Jan 2999 12:00:00 GMT\x94h\x10h\x11h\x12h\x08h\x13hih\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhlh\x1c\x8c\nthirteenth\x94h\x1ehpubs\x8c\x0emaxagetest.com\x94h\x08\x86\x94h\x05)\x81\x94\x8c\x0emax-age-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13hqh\x14\x8c\x0260\x94h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bhth\x1c\x8c\nfourteenth\x94h\x1ehxubs\x8c\x12invalid-values.com\x94h\x08\x86\x94h\x05)\x81\x94(\x8c\x16invalid-max-age-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13hyh\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh|h\x1c\x8c\tfifteenth\x94h\x1eh\x7fub\x8c\x16invalid-expires-cookie\x94h\r)\x81\x94(h\x0fh\x08h\x10h\x11h\x12h\x08h\x13\x8c\x12invalid-values.com\x94h\x14h\x08h\x15h\x08h\x16h\x08h\x17h\x08h\x18h\x08h\x19h\x08u}\x94(h\x1bh\x80h\x1c\x8c\tsixteenth\x94h\x1eh\x84ubuu.'

    cookies = pickle.loads(pickled)

    cj = CookieJar()
    cj.update_cookies(cookies_to_send)

    assert cookies == cj._cookies


@pytest.mark.parametrize(
    "url",
    [
        "http://127.0.0.1/index.html",
        URL("http://127.0.0.1/index.html"),
        ["http://127.0.0.1/index.html"],
        [URL("http://127.0.0.1/index.html")],
    ],
)
async def test_treat_as_secure_origin_init(url) -> None:
    jar = CookieJar(unsafe=True, treat_as_secure_origin=url)
    assert jar._treat_as_secure_origin == [URL("http://127.0.0.1")]


async def test_treat_as_secure_origin() -> None:
    endpoint = URL("http://127.0.0.1/")

    jar = CookieJar(unsafe=True, treat_as_secure_origin=[endpoint])
    secure_cookie = SimpleCookie(
        "cookie-key=cookie-value; HttpOnly; Path=/; Secure",
    )

    jar.update_cookies(
        secure_cookie,
        endpoint,
    )

    assert len(jar) == 1
    filtered_cookies = jar.filter_cookies(request_url=endpoint)
    assert len(filtered_cookies) == 1


async def test_filter_cookies_does_not_leak_memory() -> None:
    """Test that filter_cookies doesn't create empty cookie entries.

    Regression test for https://github.com/aio-libs/aiohttp/issues/11052
    """
    jar = CookieJar()

    # Set a cookie with Path=/
    jar.update_cookies({"test_cookie": "value; Path=/"}, URL("http://example.com/"))

    # Check initial state
    assert len(jar) == 1
    initial_storage_size = len(jar._cookies)
    initial_morsel_cache_size = len(jar._morsel_cache)

    # Make multiple requests with different paths
    paths = [
        "/",
        "/api",
        "/api/v1",
        "/api/v1/users",
        "/api/v1/users/123",
        "/static/css/style.css",
        "/images/logo.png",
    ]

    for path in paths:
        url = URL(f"http://example.com{path}")
        filtered = jar.filter_cookies(url)
        # Should still get the cookie
        assert len(filtered) == 1
        assert "test_cookie" in filtered

    # Storage size should not grow significantly
    # Only the shared cookie entry ('', '') may be added
    final_storage_size = len(jar._cookies)
    assert final_storage_size <= initial_storage_size + 1

    # Verify _morsel_cache doesn't leak either
    # It should only have entries for domains/paths where cookies exist
    final_morsel_cache_size = len(jar._morsel_cache)
    assert final_morsel_cache_size <= initial_morsel_cache_size + 1

    # Verify no empty entries were created for domain-path combinations
    for key, cookies in jar._cookies.items():
        if key != ("", ""):  # Skip the shared cookie entry
            assert len(cookies) > 0, f"Empty cookie entry found for {key}"

    # Verify _morsel_cache entries correspond to actual cookies
    for key, morsels in jar._morsel_cache.items():
        assert key in jar._cookies, f"Orphaned morsel cache entry for {key}"
        assert len(morsels) > 0, f"Empty morsel cache entry found for {key}"


async def test_update_cookies_from_headers() -> None:
    """Test update_cookies_from_headers method."""
    jar: CookieJar = CookieJar()
    url: URL = URL("http://example.com/path")

    # Test with simple cookies
    headers = [
        "session-id=123456; Path=/",
        "user-pref=dark-mode; Domain=.example.com",
        "tracking=xyz789; Secure; HttpOnly",
    ]

    jar.update_cookies_from_headers(headers, url)

    # Verify all cookies were added to the jar
    assert len(jar) == 3

    # Check cookies available for HTTP URL (secure cookie should be filtered out)
    filtered_http: BaseCookie[str] = jar.filter_cookies(url)
    assert len(filtered_http) == 2
    assert "session-id" in filtered_http
    assert filtered_http["session-id"].value == "123456"
    assert "user-pref" in filtered_http
    assert filtered_http["user-pref"].value == "dark-mode"
    assert "tracking" not in filtered_http  # Secure cookie not available on HTTP

    # Check cookies available for HTTPS URL (all cookies should be available)
    url_https: URL = URL("https://example.com/path")
    filtered_https: BaseCookie[str] = jar.filter_cookies(url_https)
    assert len(filtered_https) == 3
    assert "tracking" in filtered_https
    assert filtered_https["tracking"].value == "xyz789"


async def test_update_cookies_from_headers_duplicate_names() -> None:
    """Test that duplicate cookie names with different domains are preserved."""
    jar: CookieJar = CookieJar()
    url: URL = URL("http://www.example.com/")

    # Headers with duplicate names but different domains
    headers = [
        "session-id=123456; Domain=.example.com; Path=/",
        "session-id=789012; Domain=.www.example.com; Path=/",
        "user-pref=light; Domain=.example.com",
        "user-pref=dark; Domain=sub.example.com",
    ]

    jar.update_cookies_from_headers(headers, url)

    # Should have 3 cookies (user-pref=dark for sub.example.com is rejected)
    assert len(jar) == 3

    # Verify we have both session-id cookies
    all_cookies: list[Morsel[str]] = list(jar)
    session_ids: list[Morsel[str]] = [c for c in all_cookies if c.key == "session-id"]
    assert len(session_ids) == 2

    # Check their domains are different
    domains: set[str] = {c["domain"] for c in session_ids}
    assert domains == {"example.com", "www.example.com"}


async def test_update_cookies_from_headers_invalid_cookies(
    caplog: pytest.LogCaptureFixture,
) -> None:
    """Test that invalid cookies are logged and skipped."""
    jar: CookieJar = CookieJar()
    url: URL = URL("http://example.com/")

    # Mix of valid and invalid cookies
    headers = [
        "valid-cookie=value123",
        "invalid,cookie=value; "  # Comma character is not allowed
        "HttpOnly; Path=/",
        "another-valid=value456",
    ]

    # Enable logging for the client logger
    with caplog.at_level(logging.WARNING, logger="aiohttp.client"):
        jar.update_cookies_from_headers(headers, url)

    # Check that we logged warnings for invalid cookies
    assert "Can not load cookies" in caplog.text

    # Valid cookies should still be added
    assert len(jar) >= 2  # At least the two clearly valid cookies
    filtered: BaseCookie[str] = jar.filter_cookies(url)
    assert "valid-cookie" in filtered
    assert "another-valid" in filtered


async def test_update_cookies_from_headers_with_curly_braces() -> None:
    """Test that cookies with curly braces in names are now accepted (#2683)."""
    jar: CookieJar = CookieJar()
    url: URL = URL("http://example.com/")

    # Cookie names with curly braces should now be accepted
    headers = [
        "ISAWPLB{A7F52349-3531-4DA9-8776-F74BC6F4F1BB}="
        "{925EC0B8-CB17-4BEB-8A35-1033813B0523}; "
        "HttpOnly; Path=/",
        "regular-cookie=value123",
    ]

    jar.update_cookies_from_headers(headers, url)

    # Both cookies should be added
    assert len(jar) == 2
    filtered: BaseCookie[str] = jar.filter_cookies(url)
    assert "ISAWPLB{A7F52349-3531-4DA9-8776-F74BC6F4F1BB}" in filtered
    assert "regular-cookie" in filtered


async def test_update_cookies_from_headers_with_special_chars() -> None:
    """Test that cookies with various special characters are accepted."""
    jar: CookieJar = CookieJar()
    url: URL = URL("http://example.com/")

    # Various special characters that should now be accepted
    headers = [
        "cookie_with_parens=(value)=test123",
        "cookie-with-brackets[index]=value456",
        "cookie@with@at=value789",
        "cookie:with:colons=value000",
    ]

    jar.update_cookies_from_headers(headers, url)

    # All cookies should be added
    assert len(jar) == 4
    filtered: BaseCookie[str] = jar.filter_cookies(url)
    assert "cookie_with_parens" in filtered
    assert "cookie-with-brackets[index]" in filtered
    assert "cookie@with@at" in filtered
    assert "cookie:with:colons" in filtered


async def test_update_cookies_from_headers_empty_list() -> None:
    """Test that empty header list is handled gracefully."""
    jar: CookieJar = CookieJar()
    url: URL = URL("http://example.com/")

    # Should not raise any errors
    jar.update_cookies_from_headers([], url)

    assert len(jar) == 0


async def test_update_cookies_from_headers_with_attributes() -> None:
    """Test cookies with various attributes are handled correctly."""
    jar: CookieJar = CookieJar()
    url: URL = URL("https://secure.example.com/app/page")

    headers = [
        "secure-cookie=value1; Secure; HttpOnly; SameSite=Strict",
        "expiring-cookie=value2; Max-Age=3600; Path=/app",
        "domain-cookie=value3; Domain=.example.com; Path=/",
        "dated-cookie=value4; Expires=Wed, 09 Jun 2030 10:18:14 GMT",
    ]

    jar.update_cookies_from_headers(headers, url)

    # All cookies should be stored
    assert len(jar) == 4

    # Verify secure cookie (should work on HTTPS subdomain)
    # Note: cookies without explicit path get path from URL (/app)
    filtered_https_root: BaseCookie[str] = jar.filter_cookies(
        URL("https://secure.example.com/")
    )
    assert len(filtered_https_root) == 1  # Only domain-cookie has Path=/
    assert "domain-cookie" in filtered_https_root

    # Check app path
    filtered_https_app: BaseCookie[str] = jar.filter_cookies(
        URL("https://secure.example.com/app/")
    )
    assert len(filtered_https_app) == 4  # All cookies match
    assert "secure-cookie" in filtered_https_app
    assert "expiring-cookie" in filtered_https_app
    assert "domain-cookie" in filtered_https_app
    assert "dated-cookie" in filtered_https_app

    # Secure cookie should not be available on HTTP
    filtered_http_app: BaseCookie[str] = jar.filter_cookies(
        URL("http://secure.example.com/app/")
    )
    assert "secure-cookie" not in filtered_http_app
    assert "expiring-cookie" in filtered_http_app  # Non-secure cookies still available
    assert "domain-cookie" in filtered_http_app
    assert "dated-cookie" in filtered_http_app


async def test_update_cookies_from_headers_preserves_existing() -> None:
    """Test that update_cookies_from_headers preserves existing cookies."""
    jar: CookieJar = CookieJar()
    url: URL = URL("http://example.com/")

    # Add some initial cookies
    jar.update_cookies(
        {
            "existing1": "value1",
            "existing2": "value2",
        },
        url,
    )

    # Add more cookies via headers
    headers = [
        "new-cookie1=value3",
        "new-cookie2=value4",
    ]

    jar.update_cookies_from_headers(headers, url)

    # Should have all 4 cookies
    assert len(jar) == 4
    filtered: BaseCookie[str] = jar.filter_cookies(url)
    assert "existing1" in filtered
    assert "existing2" in filtered
    assert "new-cookie1" in filtered
    assert "new-cookie2" in filtered


async def test_update_cookies_from_headers_overwrites_same_cookie() -> None:
    """Test that cookies with same name/domain/path are overwritten."""
    jar: CookieJar = CookieJar()
    url: URL = URL("http://example.com/")

    # Add initial cookie
    jar.update_cookies({"session": "old-value"}, url)

    # Update with new value via headers
    headers = ["session=new-value"]
    jar.update_cookies_from_headers(headers, url)

    # Should still have just 1 cookie with updated value
    assert len(jar) == 1
    filtered: BaseCookie[str] = jar.filter_cookies(url)
    assert filtered["session"].value == "new-value"


async def test_dummy_cookie_jar_update_cookies_from_headers() -> None:
    """Test that DummyCookieJar ignores update_cookies_from_headers."""
    jar: DummyCookieJar = DummyCookieJar()
    url: URL = URL("http://example.com/")

    headers = [
        "cookie1=value1",
        "cookie2=value2",
    ]

    # Should not raise and should not store anything
    jar.update_cookies_from_headers(headers, url)

    assert len(jar) == 0
    filtered: BaseCookie[str] = jar.filter_cookies(url)
    assert len(filtered) == 0


async def test_shared_cookie_cache_population() -> None:
    """Test that shared cookies are cached correctly."""
    jar = CookieJar(unsafe=True)

    # Create a shared cookie (no domain/path restrictions)
    sc = SimpleCookie()
    sc["shared"] = "value"
    sc["shared"]["path"] = "/"  # Will be stripped to ""

    # Update with empty URL to avoid domain being set
    jar.update_cookies(sc, URL())

    # Verify cookie is stored at shared key
    assert ("", "") in jar._cookies
    assert "shared" in jar._cookies[("", "")]

    # Filter cookies to populate cache
    filtered = jar.filter_cookies(URL("http://example.com/"))
    assert "shared" in filtered
    assert filtered["shared"].value == "value"

    # Verify cache was populated
    assert ("", "") in jar._morsel_cache
    assert "shared" in jar._morsel_cache[("", "")]

    # Verify the cached morsel is the same one returned
    cached_morsel = jar._morsel_cache[("", "")]["shared"]
    assert cached_morsel is filtered["shared"]


async def test_shared_cookie_cache_clearing_on_update() -> None:
    """Test that shared cookie cache is cleared when cookie is updated."""
    jar = CookieJar(unsafe=True)

    # Create initial shared cookie
    sc = SimpleCookie()
    sc["shared"] = "value1"
    sc["shared"]["path"] = "/"
    jar.update_cookies(sc, URL())

    # Filter to populate cache
    filtered1 = jar.filter_cookies(URL("http://example.com/"))
    assert filtered1["shared"].value == "value1"
    assert "shared" in jar._morsel_cache[("", "")]

    # Update the cookie with new value
    sc2 = SimpleCookie()
    sc2["shared"] = "value2"
    sc2["shared"]["path"] = "/"
    jar.update_cookies(sc2, URL())

    # Verify cache was cleared
    assert "shared" not in jar._morsel_cache[("", "")]

    # Filter again to verify new value
    filtered2 = jar.filter_cookies(URL("http://example.com/"))
    assert filtered2["shared"].value == "value2"

    # Verify cache was repopulated with new value
    assert "shared" in jar._morsel_cache[("", "")]


async def test_shared_cookie_cache_clearing_on_delete() -> None:
    """Test that shared cookie cache is cleared when cookies are deleted."""
    jar = CookieJar(unsafe=True)

    # Create multiple shared cookies
    sc = SimpleCookie()
    sc["shared1"] = "value1"
    sc["shared1"]["path"] = "/"
    sc["shared2"] = "value2"
    sc["shared2"]["path"] = "/"
    jar.update_cookies(sc, URL())

    # Filter to populate cache
    jar.filter_cookies(URL("http://example.com/"))
    assert "shared1" in jar._morsel_cache[("", "")]
    assert "shared2" in jar._morsel_cache[("", "")]

    # Delete one cookie using internal method
    jar._delete_cookies([("", "", "shared1")])

    # Verify cookie and its cache entry were removed
    assert "shared1" not in jar._cookies[("", "")]
    assert "shared1" not in jar._morsel_cache[("", "")]

    # Verify other cookie remains
    assert "shared2" in jar._cookies[("", "")]
    assert "shared2" in jar._morsel_cache[("", "")]


async def test_shared_cookie_cache_clearing_on_clear() -> None:
    """Test that shared cookie cache is cleared when jar is cleared."""
    jar = CookieJar(unsafe=True)

    # Create shared and domain-specific cookies
    # Shared cookie
    sc1 = SimpleCookie()
    sc1["shared"] = "shared_value"
    sc1["shared"]["path"] = "/"
    jar.update_cookies(sc1, URL())

    # Domain-specific cookie
    sc2 = SimpleCookie()
    sc2["domain_cookie"] = "domain_value"
    jar.update_cookies(sc2, URL("http://example.com/"))

    # Filter to populate caches
    jar.filter_cookies(URL("http://example.com/"))

    # Verify caches are populated
    assert ("", "") in jar._morsel_cache
    assert "shared" in jar._morsel_cache[("", "")]
    assert ("example.com", "") in jar._morsel_cache
    assert "domain_cookie" in jar._morsel_cache[("example.com", "")]

    # Clear all cookies
    jar.clear()

    # Verify all caches are cleared
    assert len(jar._morsel_cache) == 0
    assert len(jar._cookies) == 0

    # Verify filtering returns no cookies
    filtered = jar.filter_cookies(URL("http://example.com/"))
    assert len(filtered) == 0


async def test_shared_cookie_with_multiple_domains() -> None:
    """Test that shared cookies work across different domains."""
    jar = CookieJar(unsafe=True)

    # Create a truly shared cookie
    sc = SimpleCookie()
    sc["universal"] = "everywhere"
    sc["universal"]["path"] = "/"
    jar.update_cookies(sc, URL())

    # Test filtering for different domains
    domains = [
        "http://example.com/",
        "http://test.org/",
        "http://localhost/",
        "http://192.168.1.1/",  # IP address (requires unsafe=True)
    ]

    for domain_url in domains:
        filtered = jar.filter_cookies(URL(domain_url))
        assert "universal" in filtered
        assert filtered["universal"].value == "everywhere"

    # Verify cache is reused efficiently
    assert ("", "") in jar._morsel_cache
    assert "universal" in jar._morsel_cache[("", "")]


# === Security tests for restricted unpickler and JSON save/load ===


@pytest.mark.skipif(
    sys.platform in ("android", "ios"), reason="os.system not supported"
)
async def test_load_rejects_malicious_pickle(tmp_path: Path) -> None:
    """Verify CookieJar.load() blocks arbitrary code execution via pickle.

    A crafted pickle payload using os.system (or any non-cookie class)
    must be rejected by the restricted unpickler.
    """
    import os

    file_path = tmp_path / "malicious.pkl"

    class RCEPayload:
        def __reduce__(self) -> tuple[object, ...]:
            return (os.system, ("echo PWNED",))

    with open(file_path, "wb") as f:
        pickle.dump(RCEPayload(), f, pickle.HIGHEST_PROTOCOL)

    jar = CookieJar()
    with pytest.raises(pickle.UnpicklingError, match="Forbidden class"):
        jar.load(file_path)


async def test_load_rejects_eval_payload(tmp_path: Path) -> None:
    """Verify CookieJar.load() blocks eval-based pickle payloads."""
    file_path = tmp_path / "eval_payload.pkl"

    class EvalPayload:
        def __reduce__(self) -> tuple[object, ...]:
            return (eval, ("__import__('os').system('echo PWNED')",))

    with open(file_path, "wb") as f:
        pickle.dump(EvalPayload(), f, pickle.HIGHEST_PROTOCOL)

    jar = CookieJar()
    with pytest.raises(pickle.UnpicklingError, match="Forbidden class"):
        jar.load(file_path)


async def test_load_rejects_subprocess_payload(tmp_path: Path) -> None:
    """Verify CookieJar.load() blocks subprocess-based pickle payloads."""
    import subprocess

    file_path = tmp_path / "subprocess_payload.pkl"

    class SubprocessPayload:
        def __reduce__(self) -> tuple[object, ...]:
            return (subprocess.call, (["echo", "PWNED"],))

    with open(file_path, "wb") as f:
        pickle.dump(SubprocessPayload(), f, pickle.HIGHEST_PROTOCOL)

    jar = CookieJar()
    with pytest.raises(pickle.UnpicklingError, match="Forbidden class"):
        jar.load(file_path)


async def test_load_falls_back_to_pickle(
    tmp_path: Path,
    cookies_to_receive: SimpleCookie,
) -> None:
    """Verify load() falls back to restricted pickle for legacy cookie files.

    Existing cookie files saved with older versions of aiohttp used pickle.
    load() should detect that the file is not JSON and fall back to the
    restricted pickle unpickler for backward compatibility.
    """
    file_path = tmp_path / "legit.pkl"

    # Write a legacy pickle file directly (as old aiohttp save() would)
    jar_save = CookieJar()
    jar_save.update_cookies(cookies_to_receive)
    with file_path.open(mode="wb") as f:
        pickle.dump(jar_save._cookies, f, pickle.HIGHEST_PROTOCOL)

    jar_load = CookieJar()
    jar_load.load(file_path=file_path)

    jar_test = SimpleCookie()
    for cookie in jar_load:
        jar_test[cookie.key] = cookie

    assert jar_test == cookies_to_receive


async def test_save_load_json_roundtrip(
    tmp_path: Path,
    cookies_to_receive: SimpleCookie,
) -> None:
    """Verify save/load roundtrip preserves cookies via JSON format."""
    file_path = tmp_path / "cookies.json"

    jar_save = CookieJar()
    jar_save.update_cookies(cookies_to_receive)
    jar_save.save(file_path=file_path)

    jar_load = CookieJar()
    jar_load.load(file_path=file_path)

    saved_cookies = SimpleCookie()
    for cookie in jar_save:
        saved_cookies[cookie.key] = cookie

    loaded_cookies = SimpleCookie()
    for cookie in jar_load:
        loaded_cookies[cookie.key] = cookie

    assert saved_cookies == loaded_cookies


async def test_save_load_json_partitioned_cookies(tmp_path: Path) -> None:
    """Verify save/load roundtrip works with partitioned cookies."""
    file_path = tmp_path / "partitioned.json"

    jar_save = CookieJar()
    jar_save.update_cookies_from_headers(
        ["session=cookie; Partitioned"], URL("https://example.com/")
    )
    jar_save.save(file_path=file_path)

    jar_load = CookieJar()
    jar_load.load(file_path=file_path)

    # Compare individual cookie values (same approach as test_save_load_partitioned_cookies)
    saved = list(jar_save)
    loaded = list(jar_load)
    assert len(saved) == len(loaded)
    for s, lo in zip(saved, loaded):
        assert s.key == lo.key
        assert s.value == lo.value
        assert s["domain"] == lo["domain"]
        assert s["path"] == lo["path"]


async def test_save_load_json_preserves_host_only_scope(tmp_path: Path) -> None:
    """Verify save/load keeps host-only cookies off subdomains."""
    file_path = tmp_path / "host_only.json"
    issuer = URL("https://auth.example.com/login")
    subdomain = URL("https://sub.auth.example.com/")

    jar_save = CookieJar()
    jar_save.update_cookies({"sid": "hostonly"}, response_url=issuer)
    assert "sid" not in jar_save.filter_cookies(subdomain)
    jar_save.save(file_path=file_path)

    jar_load = CookieJar()
    jar_load.load(file_path=file_path)

    assert jar_load.host_only_cookies == frozenset({("auth.example.com", "sid")})
    assert "sid" not in jar_load.filter_cookies(subdomain)
    assert "sid" in jar_load.filter_cookies(issuer)


async def test_save_load_json_domain_cookie_still_matches_subdomain(
    tmp_path: Path,
) -> None:
    """Verify save/load keeps an explicit Domain cookie valid for subdomains."""
    file_path = tmp_path / "domain.json"
    subdomain = URL("https://sub.example.com/")

    jar_save = CookieJar()
    jar_save.update_cookies_from_headers(
        ["sid=domaincookie; Domain=example.com"], URL("https://example.com/")
    )
    jar_save.save(file_path=file_path)

    jar_load = CookieJar()
    jar_load.load(file_path=file_path)

    assert jar_load.host_only_cookies == frozenset()
    assert "sid" in jar_load.filter_cookies(subdomain)


async def test_save_load_json_preserves_max_age_deadline(tmp_path: Path) -> None:
    """Verify save/load restores the absolute deadline without resetting it."""
    file_path = tmp_path / "max_age.json"
    url = URL("https://example.com/")

    jar_save = CookieJar()
    jar_save.update_cookies_from_headers(
        ["sid=x; Max-Age=3600; Domain=example.com"], url
    )
    expirations = dict(jar_save._expirations)
    jar_save.save(file_path=file_path)

    jar_load = CookieJar()
    jar_load.load(file_path=file_path)

    # The deadline is restored as the original absolute time, not now + Max-Age.
    assert dict(jar_load._expirations) == expirations
    assert "sid" in jar_load.filter_cookies(url)


async def test_save_load_json_drops_expired_cookie(tmp_path: Path) -> None:
    """Verify a cookie whose persisted deadline is in the past is dropped on load."""
    file_path = tmp_path / "expired.json"
    url = URL("https://example.com/")

    # Save a future-expiring cookie, then rewrite its persisted deadline to the
    # past so the cookie survives save() and the drop happens on the load path.
    jar_save = CookieJar()
    jar_save.update_cookies_from_headers(
        ["sid=x; Expires=Tue, 1 Jan 2999 12:00:00 GMT; Domain=example.com"], url
    )
    jar_save.save(file_path=file_path)
    data = json.loads(file_path.read_text())
    _, cookies = next(iter(data.items()))
    cookies["sid"]["expires_timestamp"] = 0.0
    file_path.write_text(json.dumps(data))

    jar_load = CookieJar()
    jar_load.load(file_path=file_path)

    assert len(jar_load) == 0
    assert "sid" not in jar_load.filter_cookies(url)


async def test_save_load_json_preserves_expires_deadline(tmp_path: Path) -> None:
    """Verify a future Expires deadline survives a save/load roundtrip."""
    file_path = tmp_path / "expires.json"
    url = URL("https://example.com/")

    jar_save = CookieJar()
    jar_save.update_cookies_from_headers(
        ["sid=x; Expires=Tue, 1 Jan 2999 12:00:00 GMT; Domain=example.com"], url
    )
    expirations = dict(jar_save._expirations)
    jar_save.save(file_path=file_path)

    jar_load = CookieJar()
    jar_load.load(file_path=file_path)

    assert dict(jar_load._expirations) == expirations
    assert "sid" in jar_load.filter_cookies(url)


async def test_load_json_old_format_without_new_keys(tmp_path: Path) -> None:
    """Verify a file written by an older version (no host_only/expires_timestamp) loads."""
    file_path = tmp_path / "old.json"
    # Old schema: no host_only, no expires_timestamp; relative max-age morsel attr.
    file_path.write_text(
        json.dumps(
            {
                "example.com|/": {
                    "sid": {
                        "key": "sid",
                        "value": "x",
                        "coded_value": "x",
                        "domain": "example.com",
                        "max-age": "3600",
                    }
                }
            }
        )
    )
    url = URL("https://example.com/")

    jar_load = CookieJar()
    # No exception when the new keys are absent.
    jar_load.load(file_path=file_path)

    # A host-only cookie saved without Domain by an older version had no domain
    # field, so it now loads as a domain cookie (the documented migration loss).
    assert "sid" in jar_load.filter_cookies(url)
    # max-age is rescheduled from load time rather than an absolute deadline.
    assert any(key[2] == "sid" for key in jar_load._expirations)


async def test_json_format_is_safe(tmp_path: Path) -> None:
    """Verify the JSON file format cannot execute code on load."""
    import json

    file_path = tmp_path / "safe.json"

    # Write something that might look dangerous but is just data
    malicious_data = {
        "evil.com|/": {
            "session": {
                "key": "session",
                "value": "__import__('os').system('echo PWNED')",
                "coded_value": "__import__('os').system('echo PWNED')",
            }
        }
    }
    with open(file_path, "w") as f:
        json.dump(malicious_data, f)

    jar = CookieJar()
    jar.load(file_path=file_path)

    # The "malicious" string is just a cookie value, not executed code
    cookies = list(jar)
    assert len(cookies) == 1
    assert cookies[0].value == "__import__('os').system('echo PWNED')"


async def test_save_load_json_secure_cookies(tmp_path: Path) -> None:
    """Verify save/load preserves Secure and HttpOnly flags."""
    file_path = tmp_path / "secure.json"

    jar_save = CookieJar()
    jar_save.update_cookies_from_headers(
        ["token=abc123; Secure; HttpOnly; Path=/; Domain=example.com"],
        URL("https://example.com/"),
    )
    jar_save.save(file_path=file_path)

    jar_load = CookieJar()
    jar_load.load(file_path=file_path)

    loaded_cookies = list(jar_load)
    assert len(loaded_cookies) == 1
    cookie = loaded_cookies[0]
    assert cookie.key == "token"
    assert cookie.value == "abc123"
    assert cookie["secure"] is True
    assert cookie["httponly"] is True
    assert cookie["domain"] == "example.com"


@pytest.mark.skipif(
    os.name != "posix", reason="POSIX permission bits are required for this test"
)
def test_save_creates_private_cookie_file(tmp_path: Path, loop) -> None:
    file_path = tmp_path / "private-cookies.json"
    jar = CookieJar(loop=loop)
    jar.update_cookies_from_headers(
        ["token=abc123; Path=/"], URL("https://example.com/")
    )

    jar.save(file_path=file_path)

    assert file_path.exists()
    assert stat.S_IMODE(file_path.stat().st_mode) == 0o600


@pytest.mark.skipif(
    os.name != "posix", reason="POSIX permission bits are required for this test"
)
def test_save_preserves_existing_cookie_file_permissions(tmp_path: Path, loop) -> None:
    file_path = tmp_path / "existing-cookies.json"
    file_path.write_text("{}", encoding="utf-8")
    file_path.chmod(0o644)

    jar = CookieJar(loop=loop)
    jar.update_cookies_from_headers(
        ["token=abc123; Path=/"], URL("https://example.com/")
    )

    jar.save(file_path=file_path)

    assert stat.S_IMODE(file_path.stat().st_mode) == 0o644


async def test_cookie_jar_unsafe_property() -> None:
    jar_safe = CookieJar()
    assert jar_safe.unsafe is False

    jar_unsafe = CookieJar(unsafe=True)
    assert jar_unsafe.unsafe is True
