/*
 * Copyright (c) 2025, Idan Horowitz <idan.horowitz@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibHTTP/Cookie/Cookie.h>
#include <LibHTTP/Cookie/ParsedCookie.h>
#include <LibJS/Runtime/Array.h>
#include <LibURL/Parser.h>
#include <LibWeb/Bindings/CookieStore.h>
#include <LibWeb/Bindings/Intrinsics.h>
#include <LibWeb/CookieStore/CookieChangeEvent.h>
#include <LibWeb/CookieStore/CookieStore.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/HTML/Scripting/Environments.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/WebIDL/DOMException.h>
#include <LibWeb/WebIDL/Promise.h>

namespace Web::CookieStore {

GC_DEFINE_ALLOCATOR(CookieStore);

CookieStore::CookieStore(JS::Realm& realm, PageClient& client)
    : DOM::EventTarget(realm)
    , m_client(client)
{
}

void CookieStore::initialize(JS::Realm& realm)
{
    WEB_SET_PROTOTYPE_FOR_INTERFACE(CookieStore);
    Base::initialize(realm);
}

void CookieStore::visit_edges(Cell::Visitor& visitor)
{
    Base::visit_edges(visitor);

    visitor.visit(m_client);
}

// https://cookiestore.spec.whatwg.org/#create-a-cookielistitem
static Bindings::CookieListItem create_a_cookie_list_item(HTTP::Cookie::Cookie const& cookie)
{
    // 1. Let name be the result of running UTF-8 decode without BOM on cookie’s name.
    // 2. Let value be the result of running UTF-8 decode without BOM on cookie’s value.
    // 3. Return «[ "name" → name, "value" → value ]»
    return Bindings::CookieListItem {
        .name = cookie.name,
        .value = cookie.value,
    };
}

// https://cookiestore.spec.whatwg.org/#normalize-a-cookie-name-or-value
static String normalize(String const& input)
{
    // Remove all U+0009 TAB and U+0020 SPACE that are at the start or end of input.
    return MUST(input.trim("\t "sv));
}

// https://cookiestore.spec.whatwg.org/#query-cookies
static Vector<Bindings::CookieListItem> query_cookies(PageClient& client, URL::URL const& url, Optional<String> const& name)
{
    // 1. Perform the steps defined in Cookies § Retrieval Model to compute the "cookie-string from a given cookie store"
    //    with url as request-uri. The cookie-string itself is ignored, but the intermediate cookie-list is used in subsequent steps.
    //    For the purposes of the steps, the cookie-string is being generated for a "non-HTTP" API.
    auto cookie_list = client.page_did_request_all_cookies_cookiestore(url);

    // 2. Let list be a new list.
    Vector<Bindings::CookieListItem> list;

    // 3. For each cookie in cookie-list, run these steps:
    for (auto const& cookie : cookie_list) {
        // 1. Assert: cookie’s http-only-flag is false.
        VERIFY(!cookie.http_only);

        // 2. If name is non-null:
        if (name.has_value()) {
            // 1. Normalize name.
            auto normalized_name = normalize(name.value());

            // 2. Let cookieName be the result of running UTF-8 decode without BOM on cookie’s name.
            // 3. If cookieName does not equal name, then continue.
            if (cookie.name != normalized_name)
                continue;
        }
        // 3. Let item be the result of running create a CookieListItem from cookie.
        auto item = create_a_cookie_list_item(cookie);

        // 4. Append item to list.
        list.append(move(item));
    }

    // 4. Return list.
    return list;
}

// https://cookiestore.spec.whatwg.org/#dom-cookiestore-get
GC::Ref<WebIDL::Promise> CookieStore::get(String name)
{
    auto& realm = this->realm();

    // 1. Let settings be this’s relevant settings object.
    auto const& settings = HTML::relevant_settings_object(*this);

    // 2. Let origin be settings’s origin.
    auto const& origin = settings.origin();

    // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
    if (origin.is_opaque())
        return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));

    // 4. Let url be settings’s creation URL.
    auto url = settings.creation_url;

    // 5. Let p be a new promise.
    auto promise = WebIDL::create_promise(realm);

    // 6. Run the following steps in parallel:
    Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = move(name)]() {
        // 1. Let list be the results of running query cookies with url and name.
        auto list = query_cookies(client, url, name);

        // AD-HOC: Queue a global task to perform the next steps
        // Spec issue: https://github.com/whatwg/cookiestore/issues/239
        queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, list = move(list)]() {
            HTML::TemporaryExecutionContext execution_context { realm };
            // 2. If list is failure, then reject p with a TypeError and abort these steps.

            // 3. If list is empty, then resolve p with null.
            if (list.is_empty())
                WebIDL::resolve_promise(realm, promise, JS::js_null());

            // 4. Otherwise, resolve p with the first item of list.
            else
                WebIDL::resolve_promise(realm, promise, Bindings::cookie_list_item_to_value(realm, list[0]));
        }));
    }));

    // 7. Return p.
    return promise;
}

// https://cookiestore.spec.whatwg.org/#dom-cookiestore-get-options
GC::Ref<WebIDL::Promise> CookieStore::get(Bindings::CookieStoreGetOptions const& options)
{
    auto& realm = this->realm();

    // 1. Let settings be this’s relevant settings object.
    auto const& settings = HTML::relevant_settings_object(*this);

    // 2. Let origin be settings’s origin.
    auto const& origin = settings.origin();

    // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
    if (origin.is_opaque())
        return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));

    // 4. Let url be settings’s creation URL.
    auto url = settings.creation_url;

    // 5. If options is empty, then return a promise rejected with a TypeError.
    if (!options.name.has_value() && !options.url.has_value())
        return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "CookieStoreGetOptions is empty"sv));

    // 6. If options["url"] is present, then run these steps:
    if (options.url.has_value()) {
        // 1. Let parsed be the result of parsing options["url"] with settings’s API base URL.
        auto parsed = URL::Parser::basic_parse(options.url.value(), settings.api_base_url());

        // AD-HOC: This isn't explicitly mentioned in the specification, but we have to reject invalid URLs as well
        if (!parsed.has_value())
            return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url is invalid"sv));

        // 2. If this’s relevant global object is a Window object and parsed does not equal url with exclude fragments
        //    set to true, then return a promise rejected with a TypeError.
        if (is<HTML::Window>(HTML::relevant_global_object(*this)) && !parsed->equals(url, URL::ExcludeFragment::Yes))
            return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url does not match creation URL"sv));

        // 3. If parsed’s origin and url’s origin are not the same origin, then return a promise rejected with a TypeError.
        if (parsed->origin() != url.origin())
            return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url's origin does not match creation URL's origin"sv));

        // 4. Set url to parsed.
        url = parsed.value();
    }

    // 7. Let p be a new promise.
    auto promise = WebIDL::create_promise(realm);

    // 8. Run the following steps in parallel:
    Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = options.name]() {
        // 1. Let list be the results of running query cookies with url and options["name"] with default null.
        auto list = query_cookies(client, url, name);

        // AD-HOC: Queue a global task to perform the next steps
        // Spec issue: https://github.com/whatwg/cookiestore/issues/239
        queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, list = move(list)]() {
            HTML::TemporaryExecutionContext execution_context { realm };
            // 2. If list is failure, then reject p with a TypeError and abort these steps.

            // 3. If list is empty, then resolve p with null.
            if (list.is_empty())
                WebIDL::resolve_promise(realm, promise, JS::js_null());

            // 4. Otherwise, resolve p with the first item of list.
            else
                WebIDL::resolve_promise(realm, promise, Bindings::cookie_list_item_to_value(realm, list[0]));
        }));
    }));

    // 9. Return p.
    return promise;
}

static JS::Value cookie_list_to_value(JS::Realm& realm, Vector<Bindings::CookieListItem> const& cookie_list)
{
    return JS::Array::create_from<Bindings::CookieListItem>(realm, cookie_list, [&](auto const& cookie) {
        return Bindings::cookie_list_item_to_value(realm, cookie);
    });
}

// https://cookiestore.spec.whatwg.org/#dom-cookiestore-getall
GC::Ref<WebIDL::Promise> CookieStore::get_all(String name)
{
    auto& realm = this->realm();

    // 1. Let settings be this’s relevant settings object.
    auto const& settings = HTML::relevant_settings_object(*this);

    // 2. Let origin be settings’s origin.
    auto const& origin = settings.origin();

    // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
    if (origin.is_opaque())
        return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));

    // 4. Let url be settings’s creation URL.
    auto url = settings.creation_url;

    // 5. Let p be a new promise.
    auto promise = WebIDL::create_promise(realm);

    // 6. Run the following steps in parallel:
    Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = move(name)]() {
        // 1. Let list be the results of running query cookies with url and name.
        auto list = query_cookies(client, url, name);

        // AD-HOC: Queue a global task to perform the next steps
        // Spec issue: https://github.com/whatwg/cookiestore/issues/239
        queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, list = move(list)]() {
            HTML::TemporaryExecutionContext execution_context { realm };
            // 2. If list is failure, then reject p with a TypeError and abort these steps.

            // 3. Otherwise, resolve p with list.
            WebIDL::resolve_promise(realm, promise, cookie_list_to_value(realm, list));
        }));
    }));

    // 7. Return p.
    return promise;
}

// https://cookiestore.spec.whatwg.org/#dom-cookiestore-getall-options
GC::Ref<WebIDL::Promise> CookieStore::get_all(Bindings::CookieStoreGetOptions const& options)
{
    auto& realm = this->realm();

    // 1. Let settings be this’s relevant settings object.
    auto const& settings = HTML::relevant_settings_object(*this);

    // 2. Let origin be settings’s origin.
    auto const& origin = settings.origin();

    // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
    if (origin.is_opaque())
        return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));

    // 4. Let url be settings’s creation URL.
    auto url = settings.creation_url;

    // 5. If options["url"] is present, then run these steps:
    if (options.url.has_value()) {
        // 1. Let parsed be the result of parsing options["url"] with settings’s API base URL.
        auto parsed = URL::Parser::basic_parse(options.url.value(), settings.api_base_url());

        // AD-HOC: This isn't explicitly mentioned in the specification, but we have to reject invalid URLs as well
        if (!parsed.has_value())
            return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url is invalid"sv));

        // 2. If this’s relevant global object is a Window object and parsed does not equal url with exclude fragments
        //    set to true, then return a promise rejected with a TypeError.
        if (is<HTML::Window>(HTML::relevant_global_object(*this)) && !parsed->equals(url, URL::ExcludeFragment::Yes))
            return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url does not match creation URL"sv));

        // 3. If parsed’s origin and url’s origin are not the same origin, then return a promise rejected with a TypeError.
        if (parsed->origin() != url.origin())
            return WebIDL::create_rejected_promise(realm, JS::TypeError::create(realm, "url's origin does not match creation URL's origin"sv));

        // 4. Set url to parsed.
        url = parsed.value();
    }

    // 6. Let p be a new promise.
    auto promise = WebIDL::create_promise(realm);

    // 7. Run the following steps in parallel:
    Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = options.name]() {
        // 1. Let list be the results of running query cookies with url and options["name"] with default null.
        auto list = query_cookies(client, url, name);

        // AD-HOC: Queue a global task to perform the next steps
        // Spec issue: https://github.com/whatwg/cookiestore/issues/239
        queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, list = move(list)]() {
            HTML::TemporaryExecutionContext execution_context { realm };
            // 2. If list is failure, then reject p with a TypeError and abort these steps.

            // 3. Otherwise, resolve p with list.
            WebIDL::resolve_promise(realm, promise, cookie_list_to_value(realm, list));
        }));
    }));

    // 8. Return p.
    return promise;
}

// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-layered-cookies#name-cookie-default-path
static Vector<String> cookie_default_path(Vector<String> path)
{
    // 1. Assert: path is a non-empty list.
    VERIFY(!path.is_empty());

    // 2. If path's size is greater than 1, then remove path's last item.
    if (path.size() > 1)
        path.take_last();

    // 3. Otherwise, set path[0] to the empty string.
    else
        path[0] = ""_string;

    // 4. Return path.
    return path;
}

// https://fetch.spec.whatwg.org/#serialized-cookie-default-path
static String serialized_cookie_default_path(URL::URL const& url)
{
    // 1. Let cloneURL be a clone of url.
    auto clone_url = url;

    // 2. Set cloneURL’s path to the cookie default path of cloneURL’s path.
    clone_url.set_raw_paths(cookie_default_path(clone_url.paths()));

    // 3. Return the URL path serialization of cloneURL.
    return clone_url.serialize_path();
}

static constexpr size_t maximum_name_value_pair_size = 4096;
static constexpr size_t maximum_attribute_value_size = 1024;

// https://cookiestore.spec.whatwg.org/#set-a-cookie
static bool set_a_cookie(PageClient& client, URL::URL const& url, String name, String value, Optional<HighResolutionTime::DOMHighResTimeStamp> expires, Optional<String> const& domain, String path, Bindings::CookieSameSite same_site, bool partitioned)
{
    // 1. Normalize name.
    name = normalize(name);

    // 2. Normalize value.
    value = normalize(value);

    // 3. If name or value contain U+003B (;), any C0 control character except U+0009 TAB, or U+007F DELETE, then return failure.
    if (name.contains(';') || value.contains(';'))
        return false;
    for (auto c = '\x00'; c <= '\x1F'; ++c) {
        if (c == '\t')
            continue;
        if (name.contains(c) || value.contains(c))
            return false;
    }
    if (name.contains('\x7F') || value.contains('\x7F'))
        return false;

    // 4. If name contains U+003D (=), then return failure.
    if (name.contains('='))
        return false;

    // 5. If name’s length is 0:
    if (name.is_empty()) {
        // 1. If value contains U+003D (=), then return failure.
        if (value.contains('='))
            return false;

        // 2. If value’s length is 0, then return failure.
        if (value.is_empty())
            return false;

        // 3. If value, byte-lowercased, starts with `__host-`, `__host-http-`, `__http-`, or `__secure-`, then return failure.
        auto value_byte_lowercased = value.to_ascii_lowercase();
        if (value_byte_lowercased.starts_with_bytes("__host-"sv) || value_byte_lowercased.starts_with_bytes("__host-http-"sv) || value_byte_lowercased.starts_with_bytes("__http-"sv) || value_byte_lowercased.starts_with_bytes("__secure-"sv))
            return false;
    }

    // 6. If name, byte-lowercased, starts with `__host-http-` or `__http-`, then return failure.
    auto name_byte_lowercased = name.to_ascii_lowercase();
    if (name_byte_lowercased.starts_with_bytes("__host-http-"sv) || name_byte_lowercased.starts_with_bytes("__http-"sv))
        return false;

    // 7. Let encodedName be the result of UTF-8 encoding name.
    // 8. Let encodedValue be the result of UTF-8 encoding value.

    // 9. If the byte sequence length of encodedName plus the byte sequence length of encodedValue is greater than the
    //    maximum name/value pair size, then return failure.
    if (name.byte_count() + value.byte_count() > maximum_name_value_pair_size)
        return false;

    // 10. Let host be url’s host
    auto const& host = url.host();

    // 11. Let attributes be a new list.
    HTTP::Cookie::ParsedCookie parsed_cookie {};
    parsed_cookie.name = move(name);
    parsed_cookie.value = move(value);

    // 12. If domain is not null, then run these steps:
    if (domain.has_value()) {
        // 1. If domain starts with U+002E (.), then return failure.
        if (domain->starts_with('.'))
            return false;

        // 2. If name, byte-lowercased, starts with `__host-`, then return failure.
        if (name_byte_lowercased.starts_with_bytes("__host-"sv))
            return false;

        // 3. If domain is not a registrable domain suffix of and is not equal to host, then return failure.
        if (!host.has_value() || !DOM::is_a_registrable_domain_suffix_of_or_is_equal_to(domain.value(), host.value()))
            return false;

        // 4. Let parsedDomain be the result of host parsing domain.
        auto parsed_domain = URL::Parser::parse_host(domain.value());

        // 5. Assert: parsedDomain is not failure.
        VERIFY(parsed_domain.has_value());

        // 6. Let encodedDomain be the result of UTF-8 encoding parsedDomain.
        auto encoded_domain = parsed_domain->serialize();

        // 7. If the byte sequence length of encodedDomain is greater than the maximum attribute value size, then return failure.
        if (encoded_domain.byte_count() > maximum_attribute_value_size)
            return false;

        // 8. Append `Domain`/encodedDomain to attributes.
        parsed_cookie.domain = move(encoded_domain);
    }

    // 13. If expires is given, then append `Expires`/expires (date serialized) to attributes.
    if (expires.has_value()) {
        auto expiry_time = UnixDateTime::from_milliseconds_since_epoch(expires.value());

        // https://www.ietf.org/archive/id/draft-ietf-httpbis-rfc6265bis-15.html#section-5.6.1
        // 3. Let cookie-age-limit be the maximum age of the cookie (which SHOULD be 400 days in the future or sooner, see
        //    Section 5.5).
        auto cookie_age_limit = UnixDateTime::now() + HTTP::Cookie::MAXIMUM_COOKIE_AGE;

        // 4. If the expiry-time is more than cookie-age-limit, the user agent MUST set the expiry time to cookie-age-limit
        //    in seconds.
        if (expiry_time.seconds_since_epoch() > cookie_age_limit.seconds_since_epoch())
            expiry_time = cookie_age_limit;

        parsed_cookie.expiry_time_from_expires_attribute = expiry_time;
    }

    // 14. If path is the empty string, then set path to the serialized cookie default path of url.
    if (path.is_empty())
        path = serialized_cookie_default_path(url);

    // 15. If path does not start with U+002F (/), then return failure.
    if (!path.starts_with('/'))
        return false;

    // 16. If path is not U+002F (/), and name, byte-lowercased, starts with `__host-`, then return failure.
    if (path != "/"sv && name_byte_lowercased.starts_with_bytes("__host-"sv))
        return false;

    // 17. Let encodedPath be the result of UTF-8 encoding path.
    // 18. If the byte sequence length of encodedPath is greater than the maximum attribute value size, then return failure.
    if (path.byte_count() > maximum_attribute_value_size)
        return false;

    // 19. Append `Path`/encodedPath to attributes.
    parsed_cookie.path = path;

    // 20. Append `Secure`/`` to attributes.
    parsed_cookie.secure_attribute_present = true;

    // 21. Switch on sameSite:
    switch (same_site) {
    // -> "none"
    case Bindings::CookieSameSite::None:
        // Append `SameSite`/`None` to attributes.
        parsed_cookie.same_site_attribute = HTTP::Cookie::SameSite::None;
        break;
    // -> "strict"
    case Bindings::CookieSameSite::Strict:
        // Append `SameSite`/`Strict` to attributes.
        parsed_cookie.same_site_attribute = HTTP::Cookie::SameSite::Strict;
        break;
    // -> "lax"
    case Bindings::CookieSameSite::Lax:
        // Append `SameSite`/`Lax` to attributes.
        parsed_cookie.same_site_attribute = HTTP::Cookie::SameSite::Lax;
        break;
    }

    // FIXME: 22. If partitioned is true, Append `Partitioned`/`` to attributes.
    (void)partitioned;

    // 23. Perform the steps defined in Cookies § Storage Model for when the user agent "receives a cookie" with url as
    //     request-uri, encodedName as cookie-name, encodedValue as cookie-value, and attributes as cookie-attribute-list.
    //     For the purposes of the steps, the newly-created cookie was received from a "non-HTTP" API.
    client.page_did_set_cookie(url, parsed_cookie, HTTP::Cookie::Source::NonHttp);

    // 24. Return success.
    return true;
}

// https://cookiestore.spec.whatwg.org/#dom-cookiestore-set
GC::Ref<WebIDL::Promise> CookieStore::set(String name, String value)
{
    auto& realm = this->realm();

    // 1. Let settings be this’s relevant settings object.
    auto const& settings = HTML::relevant_settings_object(*this);

    // 2. Let origin be settings’s origin.
    auto const& origin = settings.origin();

    // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
    if (origin.is_opaque())
        return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));

    // 4. Let url be settings’s creation URL.
    auto url = settings.creation_url;

    // 5. Let domain be null.
    // 6. Let path be "/".
    // 7. Let sameSite be strict.
    // 8. Let partitioned be false.

    // 9. Let p be a new promise.
    auto promise = WebIDL::create_promise(realm);

    // 10. Run the following steps in parallel:
    Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = move(name), value = move(value)]() {
        // 1. Let r be the result of running set a cookie with url, name, value, domain, path, sameSite, and partitioned.
        auto result = set_a_cookie(client, url, move(name), move(value), {}, {}, "/"_string, Bindings::CookieSameSite::Strict, false);

        // AD-HOC: Queue a global task to perform the next steps
        // Spec issue: https://github.com/whatwg/cookiestore/issues/239
        queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, result]() {
            HTML::TemporaryExecutionContext execution_context { realm };
            // 2. If r is failure, then reject p with a TypeError and abort these steps.
            if (!result)
                return WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Name or value are malformed"sv));

            // 3. Resolve p with undefined.
            WebIDL::resolve_promise(realm, promise);
        }));
    }));

    // 11. Return p.
    return promise;
}

// https://cookiestore.spec.whatwg.org/#dom-cookiestore-set-options
GC::Ref<WebIDL::Promise> CookieStore::set(Bindings::CookieInit const& options)
{
    auto& realm = this->realm();

    // 1. Let settings be this’s relevant settings object.
    auto const& settings = HTML::relevant_settings_object(*this);

    // 2. Let origin be settings’s origin.
    auto const& origin = settings.origin();

    // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
    if (origin.is_opaque())
        return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));

    // 4. Let url be settings’s creation URL.
    auto url = settings.creation_url;

    // 5. Let p be a new promise.
    auto promise = WebIDL::create_promise(realm);

    // 6. Run the following steps in parallel:
    Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), options = options]() {
        // 1. Let r be the result of running set a cookie with url, options["name"], options["value"], options["expires"],
        //    options["domain"], options["path"], options["sameSite"], and options["partitioned"].
        auto result = set_a_cookie(client, url, options.name, options.value, options.expires, options.domain, options.path, options.same_site, options.partitioned);

        // AD-HOC: Queue a global task to perform the next steps
        // Spec issue: https://github.com/whatwg/cookiestore/issues/239
        queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, result]() {
            HTML::TemporaryExecutionContext execution_context { realm };
            // 2. If r is failure, then reject p with a TypeError and abort these steps.
            if (!result)
                return WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Name, value, domain or path are malformed"sv));

            // 3. Resolve p with undefined.
            WebIDL::resolve_promise(realm, promise);
        }));
    }));

    // 7. Return p.
    return promise;
}

// https://cookiestore.spec.whatwg.org/#delete-a-cookie
static bool delete_a_cookie(PageClient& client, URL::URL const& url, String name, Optional<String> domain, String path, bool partitioned)
{
    // 1. Let expires be the earliest representable date represented as a timestamp.
    // NOTE: The exact value of expires is not important for the purposes of this algorithm, as long as it is in the past.
    HighResolutionTime::DOMHighResTimeStamp expires = UnixDateTime::earliest().milliseconds_since_epoch();

    // 2. Normalize name.
    name = normalize(name);

    // 3. Let value be the empty string.
    String value;

    // 4. If name’s length is 0, then set value to any non-empty implementation-defined string.
    if (name.is_empty())
        value = "ladybird"_string;

    // 5. Return the results of running set a cookie with url, name, value, expires, domain, path, "strict", and partitioned.
    return set_a_cookie(client, url, move(name), move(value), expires, move(domain), move(path), Bindings::CookieSameSite::Strict, partitioned);
}

// https://cookiestore.spec.whatwg.org/#dom-cookiestore-delete
GC::Ref<WebIDL::Promise> CookieStore::delete_(String name)
{
    auto& realm = this->realm();

    // 1. Let settings be this’s relevant settings object.
    auto const& settings = HTML::relevant_settings_object(*this);

    // 2. Let origin be settings’s origin.
    auto const& origin = settings.origin();

    // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
    if (origin.is_opaque())
        return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));

    // 4. Let url be settings’s creation URL.
    auto url = settings.creation_url;

    // 5. Let p be a new promise.
    auto promise = WebIDL::create_promise(realm);

    // 6. Run the following steps in parallel:
    Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), name = move(name)]() {
        // 1. Let r be the result of running delete a cookie with url, name, null, "/", and true.
        auto result = delete_a_cookie(client, url, move(name), {}, "/"_string, true);

        // AD-HOC: Queue a global task to perform the next steps
        // Spec issue: https://github.com/whatwg/cookiestore/issues/239
        queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, result]() {
            HTML::TemporaryExecutionContext execution_context { realm };
            // 2. If r is failure, then reject p with a TypeError and abort these steps.
            if (!result)
                return WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Name is malformed"sv));

            // 3. Resolve p with undefined.
            WebIDL::resolve_promise(realm, promise);
        }));
    }));

    // 7. Return p.
    return promise;
}

// https://cookiestore.spec.whatwg.org/#dom-cookiestore-delete-options
GC::Ref<WebIDL::Promise> CookieStore::delete_(Bindings::CookieStoreDeleteOptions const& options)
{
    auto& realm = this->realm();

    // 1. Let settings be this’s relevant settings object.
    auto const& settings = HTML::relevant_settings_object(*this);

    // 2. Let origin be settings’s origin.
    auto const& origin = settings.origin();

    // 3. If origin is an opaque origin, then return a promise rejected with a "SecurityError" DOMException.
    if (origin.is_opaque())
        return WebIDL::create_rejected_promise(realm, WebIDL::SecurityError::create(realm, "Document origin is opaque"_utf16));

    // 4. Let url be settings’s creation URL.
    auto url = settings.creation_url;

    // 5. Let p be a new promise.
    auto promise = WebIDL::create_promise(realm);

    // 6. Run the following steps in parallel:
    Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(realm.heap(), [&realm, client = m_client, promise, url = move(url), options = options]() {
        // 1. Let r be the result of running delete a cookie with url, options["name"], options["domain"], options["path"],
        //    and options["partitioned"].
        auto result = delete_a_cookie(client, url, options.name, options.domain, options.path, options.partitioned);

        // AD-HOC: Queue a global task to perform the next steps
        // Spec issue: https://github.com/whatwg/cookiestore/issues/239
        queue_global_task(HTML::Task::Source::Unspecified, realm.global_object(), GC::create_function(realm.heap(), [&realm, promise, result]() {
            HTML::TemporaryExecutionContext execution_context { realm };
            // 2. If r is failure, then reject p with a TypeError and abort these steps.
            if (!result)
                return WebIDL::reject_promise(realm, promise, JS::TypeError::create(realm, "Name is malformed"sv));

            // 3. Resolve p with undefined.
            WebIDL::resolve_promise(realm, promise);
        }));
    }));

    // 7. Return p.
    return promise;
}

void CookieStore::set_onchange(WebIDL::CallbackType* event_handler)
{
    set_event_handler_attribute(HTML::EventNames::change, event_handler);
}

WebIDL::CallbackType* CookieStore::onchange()
{
    return event_handler_attribute(HTML::EventNames::change);
}

// https://cookiestore.spec.whatwg.org/#cookie-change
struct CookieChange {
    enum class Type {
        Changed,
        Deleted,
    };

    HTTP::Cookie::Cookie cookie;
    Type type;
};

// https://cookiestore.spec.whatwg.org/#observable-changes
static Vector<CookieChange> observable_changes(Vector<HTTP::Cookie::Cookie> changes)
{
    // The observable changes for url are the set of cookie changes to cookies in a cookie store which meet the
    // requirements in step 1 of Cookies § Retrieval Algorithm’s steps to compute the "cookie-string from a given
    // cookie store" with url as request-uri, for a "non-HTTP" API.
    Vector<CookieChange> observable_changes;
    observable_changes.ensure_capacity(changes.size());

    auto now = UnixDateTime::now();

    for (auto& cookie : changes) {
        // A cookie change is a cookie and a type (either changed or deleted):
        // - A cookie which is removed due to an insertion of another cookie with the same name, domain, and path is ignored.
        // - A newly-created cookie which is not immediately evicted is considered changed.
        // - A newly-created cookie which is immediately evicted is considered deleted.
        // - A cookie which is otherwise evicted or removed is considered deleted
        auto type = cookie.expiry_time < now ? CookieChange::Type::Deleted : CookieChange::Type::Changed;
        observable_changes.unchecked_empend(move(cookie), type);
    }

    return observable_changes;
}

struct PreparedLists {
    Vector<Bindings::CookieListItem> changed_list;
    Vector<Bindings::CookieListItem> deleted_list;
};

// https://cookiestore.spec.whatwg.org/#prepare-lists
static PreparedLists prepare_lists(Vector<CookieChange> const& changes)
{
    // 1. Let changedList be a new list.
    Vector<Bindings::CookieListItem> changed_list;

    // 2. Let deletedList be a new list.
    Vector<Bindings::CookieListItem> deleted_list;

    // 3. For each change in changes, run these steps:
    for (auto const& change : changes) {
        // 1. Let item be the result of running create a CookieListItem from change’s cookie.
        auto item = create_a_cookie_list_item(change.cookie);

        // 2. If change’s type is changed, then append item to changedList.
        if (change.type == CookieChange::Type::Changed)
            changed_list.append(move(item));

        // 3. Otherwise, run these steps:
        else {
            // 1. Set item["value"] to undefined.
            item.value.clear();

            // 2. Append item to deletedList.
            deleted_list.append(move(item));
        }
    }

    // 4. Return changedList and deletedList.
    return { move(changed_list), move(deleted_list) };
}

// https://cookiestore.spec.whatwg.org/#process-cookie-changes
void CookieStore::process_cookie_changes(Vector<HTTP::Cookie::Cookie> all_changes)
{
    auto& realm = this->realm();

    // 1. Let url be window’s relevant settings object’s creation URL.
    // 2. Let changes be the observable changes for url.
    // 3. If changes is empty, then continue.
    // NB: We perform the URL-based filtering in the UI process so that we don't have to send all changed cookies over
    //     IPC to every tab.
    auto changes = observable_changes(move(all_changes));

    // 4. Queue a global task on the DOM manipulation task source given window to fire a change event named "change"
    //    with changes at window’s CookieStore.
    queue_global_task(HTML::Task::Source::DOMManipulation, realm.global_object(), GC::create_function(realm.heap(), [this, &realm, changes = move(changes)]() {
        HTML::TemporaryExecutionContext execution_context { realm };
        // https://cookiestore.spec.whatwg.org/#fire-a-change-event
        // 4. Let changedList and deletedList be the result of running prepare lists from changes.
        auto [changed_list, deleted_list] = prepare_lists(changes);

        Bindings::CookieChangeEventInit event_init = {};
        // 5. Set event’s changed attribute to changedList.
        event_init.changed = move(changed_list);

        // 6. Set event’s deleted attribute to deletedList.
        event_init.deleted = move(deleted_list);

        // 1. Let event be the result of creating an Event using CookieChangeEvent.
        // 2. Set event’s type attribute to type.
        auto event = CookieChangeEvent::create(realm, HTML::EventNames::change, event_init);

        // 3. Set event’s bubbles and cancelable attributes to false.
        event->set_bubbles(false);
        event->set_cancelable(false);

        // 7. Dispatch event at target.
        this->dispatch_event(event);
    }));
}

}
