/*
 * Copyright (c) 2022-2024, Andreas Kling <andreas@ladybird.org>
 * Copyright (c) 2023-2025, Aliaksandr Kalenik <kalenik.aliaksandr@gmail.com>
 * Copyright (c) 2025, Luke Wilde <luke@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <AK/NeverDestroyed.h>
#include <LibCore/Timer.h>
#include <LibGfx/PaintingSurface.h>
#include <LibWeb/CSS/ComputedProperties.h>
#include <LibWeb/CSS/PseudoElement.h>
#include <LibWeb/CSS/SystemColor.h>
#include <LibWeb/CSS/VisualViewport.h>
#include <LibWeb/Compositor/CompositorHost.h>
#include <LibWeb/ContentSecurityPolicy/BlockingAlgorithms.h>
#include <LibWeb/ContentSecurityPolicy/Directives/DirectiveOperations.h>
#include <LibWeb/ContentSecurityPolicy/PolicyList.h>
#include <LibWeb/ContentSecurityPolicy/Violation.h>
#include <LibWeb/Crypto/Crypto.h>
#include <LibWeb/DOM/Document.h>
#include <LibWeb/DOM/DocumentLoading.h>
#include <LibWeb/DOM/Element.h>
#include <LibWeb/DOM/Event.h>
#include <LibWeb/DOM/Position.h>
#include <LibWeb/DOM/Range.h>
#include <LibWeb/DOM/Text.h>
#include <LibWeb/Fetch/Fetching/Fetching.h>
#include <LibWeb/Fetch/Infrastructure/FetchAlgorithms.h>
#include <LibWeb/Fetch/Infrastructure/FetchController.h>
#include <LibWeb/Fetch/Infrastructure/HTTP/Requests.h>
#include <LibWeb/Fetch/Infrastructure/URL.h>
#include <LibWeb/FileAPI/File.h>
#include <LibWeb/HTML/BrowsingContext.h>
#include <LibWeb/HTML/BrowsingContextGroup.h>
#include <LibWeb/HTML/DocumentState.h>
#include <LibWeb/HTML/EventLoop/EventLoop.h>
#include <LibWeb/HTML/HTMLIFrameElement.h>
#include <LibWeb/HTML/HTMLInputElement.h>
#include <LibWeb/HTML/History.h>
#include <LibWeb/HTML/HistoryHandlingBehavior.h>
#include <LibWeb/HTML/Navigable.h>
#include <LibWeb/HTML/NavigableContainer.h>
#include <LibWeb/HTML/Navigation.h>
#include <LibWeb/HTML/NavigationObserver.h>
#include <LibWeb/HTML/NavigationParams.h>
#include <LibWeb/HTML/POSTResource.h>
#include <LibWeb/HTML/Parser/HTMLParser.h>
#include <LibWeb/HTML/PolicyContainers.h>
#include <LibWeb/HTML/SandboxingFlagSet.h>
#include <LibWeb/HTML/Scripting/ClassicScript.h>
#include <LibWeb/HTML/Scripting/TemporaryExecutionContext.h>
#include <LibWeb/HTML/SessionHistoryEntry.h>
#include <LibWeb/HTML/StructuredSerialize.h>
#include <LibWeb/HTML/TraversableNavigable.h>
#include <LibWeb/HTML/Window.h>
#include <LibWeb/HTML/WindowProxy.h>
#include <LibWeb/Infra/Strings.h>
#include <LibWeb/Layout/Node.h>
#include <LibWeb/Layout/Viewport.h>
#include <LibWeb/Loader/GeneratedPagesLoader.h>
#include <LibWeb/Page/Page.h>
#include <LibWeb/Painting/Paintable.h>
#include <LibWeb/Painting/PaintableBox.h>
#include <LibWeb/Painting/ViewportPaintable.h>
#include <LibWeb/Platform/EventLoopPlugin.h>
#include <LibWeb/Selection/Selection.h>
#include <LibWeb/UIEvents/InputTypes.h>
#include <LibWeb/WebIDL/Promise.h>
#include <LibWeb/XHR/FormData.h>

#include <AK/Debug.h>
#include <AK/StdLibExtras.h>

namespace Web::HTML {

GC_DEFINE_ALLOCATOR(Navigable);

struct NavigationParamsFetchStateHolder : public JS::Cell {
    GC_CELL(NavigationParamsFetchStateHolder, JS::Cell);
    GC_DECLARE_ALLOCATOR(NavigationParamsFetchStateHolder);

    NavigationParamsFetchStateHolder(OpenerPolicyEnforcementResult&& coop_enforcement_result, URL::URL current_url, GC::Ref<Fetch::Infrastructure::Request> request,
        Optional<URL::Origin> initiator_origin,
        Variant<SerializedPolicyContainer, DocumentState::Client> history_policy_container,
        Optional<URL::URL> about_base_url,
        GC::Ref<SourceSnapshotParams> source_snapshot_params,
        Fetch::Infrastructure::Request::ReferrerType request_referrer,
        ReferrerPolicy::ReferrerPolicy request_referrer_policy,
        Optional<URL::Origin> origin,
        Variant<Empty, String, POSTResource> resource,
        bool ever_populated,
        String navigable_target_name)
        : coop_enforcement_result(move(coop_enforcement_result))
        , current_url(move(current_url))
        , request(request)
        , initiator_origin(move(initiator_origin))
        , history_policy_container(move(history_policy_container))
        , about_base_url(move(about_base_url))
        , source_snapshot_params(source_snapshot_params)
        , request_referrer(move(request_referrer))
        , request_referrer_policy(request_referrer_policy)
        , origin(move(origin))
        , resource(move(resource))
        , ever_populated(ever_populated)
        , navigable_target_name(move(navigable_target_name))
    {
    }

    GC::Ptr<Fetch::Infrastructure::Response> response;
    Optional<URL::Origin> response_origin;
    GC::Ptr<Fetch::Infrastructure::FetchController> fetch_controller;
    OpenerPolicyEnforcementResult coop_enforcement_result;
    SandboxingFlagSet final_sandbox_flags {};
    GC::Ptr<PolicyContainer> response_policy_container;
    OpenerPolicy response_coop {};
    ErrorOr<Optional<URL::URL>> location_url { OptionalNone {} };
    URL::URL current_url;
    GC::Ptr<GC::Function<void(DOM::Document&)>> commit_early_hints;

    GC::Ref<Fetch::Infrastructure::Request> request;
    GC::Ptr<Navigable> navigable;
    ContentSecurityPolicy::Directives::Directive::NavigationType csp_navigation_type;
    TargetSnapshotParams target_snapshot_params;
    Optional<String> navigation_id;

    // Fields extracted from entry's document_state
    Optional<URL::Origin> initiator_origin;
    Variant<SerializedPolicyContainer, DocumentState::Client> history_policy_container;
    Optional<URL::URL> about_base_url;
    GC::Ref<SourceSnapshotParams> source_snapshot_params;

    // Document state fields for redirect DocumentState rebuild
    Fetch::Infrastructure::Request::ReferrerType request_referrer { Fetch::Infrastructure::Request::Referrer::Client };
    ReferrerPolicy::ReferrerPolicy request_referrer_policy { ReferrerPolicy::DEFAULT_REFERRER_POLICY };
    Optional<URL::Origin> origin;
    Variant<Empty, String, POSTResource> resource;
    bool ever_populated = false;
    String navigable_target_name;

    // Accumulated redirect output
    Optional<URL::URL> redirected_url;
    Optional<SerializationRecord> redirect_classic_history_api_state;
    RefPtr<DocumentState> replacement_document_state;
    bool resource_cleared = false;

    enum class ContinuationReason {
        GotResponse,
        OngoingNavigationChanged,
    };

    GC::Ptr<GC::Function<void(ContinuationReason)>> continuation_steps;

    virtual void visit_edges(Cell::Visitor& visitor) override
    {
        Base::visit_edges(visitor);
        visitor.visit(response);
        visitor.visit(fetch_controller);
        visitor.visit(response_policy_container);
        visitor.visit(commit_early_hints);
        visitor.visit(request);
        visitor.visit(navigable);
        visitor.visit(source_snapshot_params);
        visitor.visit(continuation_steps);
    }
};

GC_DEFINE_ALLOCATOR(NavigationParamsFetchStateHolder);

// Carries the redirect metadata alongside navigation params from the fetch path back to
// populate_session_history_entry_document's received_navigation_params callback.
struct InternalNavigationResult final : public JS::Cell {
    GC_CELL(InternalNavigationResult, JS::Cell);
    GC_DECLARE_ALLOCATOR(InternalNavigationResult);

public:
    Navigable::NavigationParamsVariant navigation_params { Navigable::NullOrError {} };

    // Redirect mutations (only set by fetch path)
    Optional<URL::URL> redirected_url;
    Optional<SerializationRecord> classic_history_api_state;
    RefPtr<DocumentState> replacement_document_state;
    bool resource_cleared = false;

private:
    virtual void visit_edges(Cell::Visitor& visitor) override
    {
        Base::visit_edges(visitor);
        visitor.visit(navigation_params);
    }
};

GC_DEFINE_ALLOCATOR(InternalNavigationResult);

GC_DEFINE_ALLOCATOR(PopulateSessionHistoryEntryDocumentOutput);

void PopulateSessionHistoryEntryDocumentOutput::apply_to(NonnullRefPtr<SessionHistoryEntry> entry)
{
    if (replacement_document_state)
        entry->set_document_state(replacement_document_state);
    if (redirected_url.has_value())
        entry->set_url(redirected_url.value());
    if (classic_history_api_state.has_value())
        entry->set_classic_history_api_state(classic_history_api_state.release_value());
    if (resource_cleared)
        entry->document_state()->set_resource(Empty {});

    // Step from populate_session_history_entry_document()
    // 7. If entry's document state's document is not null, then:
    if (document) {
        entry->document_state()->set_document_id(document->unique_id());

        // 1. Set entry's document state's ever populated to true.
        entry->document_state()->set_ever_populated(true);

        // 2. If saveExtraDocumentState is true:
        if (save_extra_document_state) {
            // 1. Set entry's document state's origin to document's origin.
            entry->document_state()->set_origin(document->origin());

            // 2. If document's URL requires storing the policy container in history, then:
            if (url_requires_storing_the_policy_container_in_history(document->url())) {
                // 1. Assert: navigationParams is a navigation params (i.e., neither null nor a non-fetch scheme navigation params).
                VERIFY(navigation_params.has<GC::Ref<NavigationParams>>());

                // 2. Set entry's document state's history policy container to navigationParams's policy container.
                entry->document_state()->set_history_policy_container(
                    navigation_params.get<GC::Ref<NavigationParams>>()->policy_container->serialize());
            }
        }

        // 3. If entry's document state's request referrer is "client", and navigationParams is a navigation params (i.e., neither null nor a non-fetch scheme navigation params), then:
        if (entry->document_state()->request_referrer() == Fetch::Infrastructure::Request::Referrer::Client
            && navigation_params.has<GC::Ref<NavigationParams>>()
            && navigation_params.get<GC::Ref<NavigationParams>>()->request) {
            // NB: We don't assert navigationParams's request is not null because srcdoc navigations create NavigationParams with a null request.

            // 1. Set entry's document state's request referrer to navigationParams's request's referrer.
            entry->document_state()->set_request_referrer(
                navigation_params.get<GC::Ref<NavigationParams>>()->request->referrer());
        }
    }
}

void PopulateSessionHistoryEntryDocumentOutput::visit_edges(Cell::Visitor& visitor)
{
    Base::visit_edges(visitor);
    visitor.visit(document);
    visitor.visit(navigation_params);
}

HashTable<GC::RawRef<Navigable>>& all_navigables()
{
    static NeverDestroyed<HashTable<GC::RawRef<Navigable>>> set;
    return *set;
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-session-history-entries
static Vector<NonnullRefPtr<SessionHistoryEntry>>* get_session_history_entries_if_present(TraversableNavigable& traversable, Navigable const& navigable)
{
    // 4. Let docStates be an empty ordered set of document states.
    Vector<RefPtr<DocumentState>> doc_states;

    // 5. For each entry of traversable's session history entries, append entry's document state to docStates.
    for (auto& entry : traversable.session_history_entries())
        doc_states.append(entry->document_state());

    // 6. For each docState of docStates:
    for (size_t i = 0; i < doc_states.size(); ++i) {
        auto doc_state = doc_states[i];

        // 1. For each nestedHistory of docState's nested histories:
        for (auto& nested_history : doc_state->nested_histories()) {
            // 1. If nestedHistory's id equals navigable's id, return nestedHistory's entries.
            if (nested_history.id == navigable.id())
                return &nested_history.entries;

            // 2. For each entry of nestedHistory's entries, append entry's document state to docStates.
            for (auto& entry : nested_history.entries)
                doc_states.append(entry->document_state());
        }
    }

    return nullptr;
}

Vector<NonnullRefPtr<SessionHistoryEntry>>* append_nested_history_for_child_navigable(
    Navigable& parent_navigable, Navigable& child_navigable, SessionHistoryEntry& history_entry)
{
    VERIFY(child_navigable.parent() == &parent_navigable);

    auto parent_doc_state = parent_navigable.active_session_history_entry()->document_state();
    auto& parent_navigable_entries = parent_navigable.get_session_history_entries();
    auto target_step_entry_iterator = parent_navigable_entries.find_if([parent_doc_state](auto& entry) {
        return entry->document_state() == parent_doc_state;
    });
    if (target_step_entry_iterator == parent_navigable_entries.end())
        return nullptr;

    history_entry.set_step((*target_step_entry_iterator)->step());

    DocumentState::NestedHistory nested_history {
        .id = child_navigable.id(),
        .entries { history_entry },
    };
    parent_doc_state->nested_histories().append(move(nested_history));
    return &parent_doc_state->nested_histories().last().entries;
}

static Vector<NonnullRefPtr<SessionHistoryEntry>>*
recreate_missing_nested_history_for_live_child_navigable(TraversableNavigable& traversable, Navigable& navigable)
{
    VERIFY(&navigable != &traversable);

    auto parent = navigable.parent();
    if (!parent)
        return nullptr;

    auto container = navigable.container();
    if (!container || container->content_navigable() != &navigable)
        return nullptr;

    auto history_entry = navigable.active_session_history_entry();
    if (!history_entry)
        return nullptr;

    return append_nested_history_for_child_navigable(*parent, navigable, *history_entry);
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#child-navigable
Vector<GC::Root<Navigable>> Navigable::child_navigables() const
{
    Vector<GC::Root<Navigable>> results;
    for (auto& entry : all_navigables()) {
        if (entry->current_session_history_entry()->step() == SessionHistoryEntry::Pending::Tag)
            continue;
        if (entry->parent() == this)
            results.append(entry);
    }

    return results;
}

bool Navigable::is_ancestor_of(GC::Ref<Navigable> other) const
{
    for (auto ancestor = other->parent(); ancestor; ancestor = ancestor->parent()) {
        if (ancestor == this)
            return true;
    }
    return false;
}

Navigable::Navigable(
    GC::Ref<Page> page,
    bool is_svg_page,
    Compositor::PagePresentationRegistration page_presentation_registration)
    : m_page(page)
    , m_event_handler({}, *this)
    , m_is_svg_page(is_svg_page)
{
    all_navigables().set(*this);

    if (!m_is_svg_page && page->has_compositor_host()) {
        auto context_id = page->client().allocate_compositor_context_id(page_presentation_registration);
        m_compositor_context = page->compositor_host().create_context(context_id);
    }
}

Navigable::~Navigable() = default;

void Navigable::set_has_been_destroyed()
{
    cancel_hover_update_after_async_scroll();
    destroy_compositor_context();
    m_has_been_destroyed = true;
    resolve_all_pending_async_scroll_operations();
}

void Navigable::remove_from_all_navigables()
{
    cancel_hover_update_after_async_scroll();
    destroy_compositor_context();
    resolve_all_pending_async_scroll_operations();

    if (m_active_document)
        m_active_document->set_navigable(nullptr);
    all_navigables().remove(*this);
}

void Navigable::finalize()
{
    cancel_hover_update_after_async_scroll();
    destroy_compositor_context();
    all_navigables().remove(*this);
    Base::finalize();
}

void Navigable::visit_edges(Cell::Visitor& visitor)
{
    Base::visit_edges(visitor);
    visitor.visit(m_page);
    visitor.visit(m_parent);
    visitor.visit(m_active_document);
    visitor.visit(m_input_method_composition_node);
    visitor.visit(m_container);
    m_event_handler.visit_edges(visitor);

    for (auto& navigation_params : m_pending_navigations) {
        navigation_params.visit_edges(visitor);
    }

    for (auto& async_scroll_operation : m_pending_async_scroll_operations)
        visitor.visit(async_scroll_operation.promise);
}

void Navigable::NavigateParams::visit_edges(Cell::Visitor& visitor)
{
    visitor.visit(response);
    visitor.visit(source_document);
    visitor.visit(source_element);
    if (form_data_entry_list.has_value()) {
        for (auto& entry : form_data_entry_list.value()) {
            entry.value.visit([&](GC::Ref<FileAPI::File> const& file) { visitor.visit(file); },
                [&](auto const&) {});
        }
    }
}

// https://html.spec.whatwg.org/multipage/nav-history-apis.html#script-closable
bool Navigable::is_script_closable()
{
    // A navigable is script-closable if it is a top-level traversable, and any of the following are true:
    // - its is created by web content is true; or
    // - its session history entries's size is 1.
    if (!is_top_level_traversable())
        return false;

    return as<TraversableNavigable>(this)->is_created_by_web_content()
        || get_session_history_entries().size() == 1;
}

void Navigable::set_delaying_load_events(bool value)
{
    if (value) {
        auto document = container_document();
        VERIFY(document);
        m_delaying_the_load_event.emplace(*document);
    } else {
        m_delaying_the_load_event.clear();
    }
}

void Navigable::set_navigation_load_event_guard(DOM::Document& parent_doc)
{
    m_navigation_load_event_guard.emplace(parent_doc);
}

void Navigable::clear_navigation_load_event_guard()
{
    m_navigation_load_event_guard.clear();
}

RefPtr<SessionHistoryEntry> Navigable::active_session_history_entry() const
{
    return m_active_session_history_entry;
}

void Navigable::set_active_session_history_entry(RefPtr<SessionHistoryEntry> entry)
{
    m_active_session_history_entry = move(entry);
}

RefPtr<SessionHistoryEntry> Navigable::current_session_history_entry() const
{
    return m_current_session_history_entry;
}

void Navigable::set_current_session_history_entry(RefPtr<SessionHistoryEntry> entry)
{
    m_current_session_history_entry = move(entry);
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#initialize-the-navigable
void Navigable::initialize_navigable(NonnullRefPtr<DocumentState> document_state, GC::Ptr<Navigable> parent, GC::Ref<DOM::Document> document)
{
    static int next_id = 0;
    m_id = String::number(next_id++);

    // 1. Assert: documentState's document is non-null.
    // NOTE: DocumentState no longer owns the document; it is passed separately and owned by the Navigable.

    // 2. Let entry be a new session history entry, with
    auto entry = SessionHistoryEntry::create();
    // URL: document's URL
    entry->set_url(document->url());
    // document state: documentState
    entry->set_document_state(document_state);
    document_state->set_document_id(document->unique_id());

    // 3. Set navigable's current session history entry to entry.
    m_current_session_history_entry = entry;

    // 4. Set navigable's active session history entry to entry.
    m_active_session_history_entry = entry;
    m_active_document = document;
    document->set_navigable(this);

    // 5. Set navigable's parent to parent.
    m_parent = parent;
    if (parent) {
        m_should_show_line_box_borders = parent->m_should_show_line_box_borders;
        m_should_show_caret_hit_test_debug_overlay = parent->m_should_show_caret_hit_test_debug_overlay;
    }
    if (parent && !m_is_svg_page && has_compositor_context() && parent->has_compositor_context()) {
        compositor_context().set_parent_context(parent->compositor_context().id());
    }

    // 6. Set the initial visibility state of documentState's document to navigable's traversable navigable's system visibility state.
    document->set_initial_visibility_state(traversable_navigable()->system_visibility_state());
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-the-target-history-entry
static RefPtr<SessionHistoryEntry> get_the_target_history_entry_from_entries(
    Vector<NonnullRefPtr<SessionHistoryEntry>> const& entries, int target_step)
{
    // 2. Return the item in entries that has the greatest step less than or equal to step.
    RefPtr<SessionHistoryEntry> result = nullptr;
    for (auto& entry : entries) {
        // NB: "pending" is not a used history step.
        // https://html.spec.whatwg.org/multipage/browsing-the-web.html#she-step
        auto entry_step = entry->step_value();
        if (entry_step.has_value() && *entry_step <= target_step) {
            if (!result || *result->step_value() < *entry_step) {
                result = entry;
            }
        }
    }

    return result;
}

RefPtr<SessionHistoryEntry> Navigable::get_the_target_history_entry(int target_step) const
{
    // 1. Let entries be the result of getting session history entries for navigable.
    auto& entries = get_session_history_entries();

    return get_the_target_history_entry_from_entries(entries, target_step);
}

RefPtr<SessionHistoryEntry> Navigable::get_the_target_history_entry_if_present(int target_step) const
{
    auto traversable = traversable_navigable();
    Vector<NonnullRefPtr<SessionHistoryEntry>>* entries = nullptr;
    if (this == traversable.ptr())
        entries = &traversable->session_history_entries();
    else
        entries = get_session_history_entries_if_present(*traversable, *this);

    // AD-HOC: The spec asserts that a nested history list is found. During queued navigable creation/destruction
    //         bookkeeping, engines can still observe a child navigable after its iframe has been removed from the
    //         parent's nested histories. In that case, the detached child has no observable session history effect.
    if (!entries)
        return nullptr;

    return get_the_target_history_entry_from_entries(*entries, target_step);
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#activate-history-entry
void Navigable::activate_history_entry(RefPtr<SessionHistoryEntry> entry, GC::Ref<DOM::Document> document)
{
    // 1. Save persisted state to the navigable's active session history entry.
    save_persisted_state_to_active_session_history_entry();

    // 2. Let newDocument be entry's document.
    auto new_document = document;

    // 3. Assert: newDocument's is initial about:blank is false, i.e., we never traverse
    //    back to the initial about:blank Document because it always gets replaced when we
    //    navigate away from it.
    VERIFY(!new_document->is_initial_about_blank());

    // 4. Set navigable's active session history entry to entry.
    m_active_session_history_entry = entry;
    if (m_active_document && m_active_document != new_document) {
        // The pending post-scroll hover refresh belongs to the outgoing document; drop it.
        cancel_hover_update_after_async_scroll();
        m_active_document->set_navigable(nullptr);
    }
    m_active_document = new_document;
    new_document->set_navigable(this);
    set_needs_to_record_display_list();

    // 5. Make active newDocument.
    new_document->make_active();

    // 6. Set the initial visibility state of newDocument to navigable's traversable navigable's system visibility state.
    new_document->set_initial_visibility_state(traversable_navigable()->system_visibility_state());

    // AD-HOC: In the async state machine, documents created during populate may have completed
    //         their loading lifecycle before being activated (when they had no navigable).
    //         Re-trigger the post-load steps that were skipped:
    //         - completely_finish_loading: fires the iframe load event on the container
    //         - clear_navigation_load_event_guard: clears the parent's load event delayer
    //           that was set in finalize_a_cross_document_navigation
    //         - schedule_html_parser_end_check: allows the parent's parser end state to progress
    if (new_document->ready_for_post_load_tasks()) {
        clear_navigation_load_event_guard();
        if (auto nav_container = container())
            nav_container->document().schedule_html_parser_end_check();
    }
    if (new_document->completely_loaded_deferred())
        new_document->completely_finish_loading();

    if (m_ongoing_navigation.has<Empty>()) {
        for (auto& navigation_observer : m_navigation_observers) {
            if (navigation_observer.navigation_complete())
                navigation_observer.navigation_complete()->function()();
        }
    }
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#save-persisted-state
void Navigable::save_persisted_state_to_active_session_history_entry()
{
    auto entry = active_session_history_entry();
    if (!entry)
        return;

    // 1. Set the scroll position data of entry to contain the scroll positions for all of entry's document's
    //    restorable scrollable regions.
    auto scroll_position_data = entry->scroll_position_data();
    scroll_position_data.viewport_scroll_position = viewport_scroll_offset();
    entry->set_scroll_position_data(move(scroll_position_data));

    // FIXME: 2. Optionally, update entry's persisted user state.
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#restore-persisted-user-state
void Navigable::restore_persisted_state_from_session_history_entry(SessionHistoryEntry const& entry)
{
    // 1. If entry's scroll restoration mode is "auto", and entry's document's relevant global object's navigation
    //    API's suppress normal scroll restoration during ongoing navigation is false, then restore scroll position
    //    data given entry.
    if (entry.scroll_restoration_mode() == ScrollRestorationMode::Auto) {
        if (auto window = active_window()) {
            if (!window->navigation()->suppress_normal_scroll_restoration_during_ongoing_navigation())
                restore_scroll_position_data(entry);
        }
    }

    // FIXME: 2. Optionally, update other aspects of entry's document and its rendering, for instance values of form
    //        fields, that the user agent had previously recorded in entry's persisted user state.
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#restore-scroll-position-data
void Navigable::restore_scroll_position_data(SessionHistoryEntry const& entry)
{
    auto const& scroll_position_data = entry.scroll_position_data();
    if (!scroll_position_data.viewport_scroll_position.has_value())
        return;

    // FIXME: If the document has been scrolled by the user, return.
    perform_scroll_of_viewport_scrolling_box(*scroll_position_data.viewport_scroll_position);
    clamp_viewport_scroll_offset();
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#nav-document
GC::Ptr<DOM::Document> Navigable::active_document() const
{
    // A navigable's active document is its active session history entry's document.
    return m_active_document;
}

Optional<UniqueNodeID> Navigable::active_document_id() const
{
    if (!m_active_document)
        return {};
    return m_active_document->unique_id();
}

void Navigable::set_active_document(GC::Ptr<DOM::Document> document)
{
    if (m_active_document && m_active_document != document) {
        // The pending post-scroll hover refresh belongs to the outgoing document; drop it.
        cancel_hover_update_after_async_scroll();
        m_active_document->set_navigable(nullptr);
    }
    m_active_document = document;
    if (document)
        document->set_navigable(this);
    set_needs_to_record_display_list();

    VERIFY(m_active_session_history_entry);
    Optional<UniqueNodeID> document_id;
    if (document)
        document_id = document->unique_id();
    m_active_session_history_entry->document_state()->set_document_id(document_id);
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#nav-bc
GC::Ptr<BrowsingContext> Navigable::active_browsing_context()
{
    // A navigable's active browsing context is its active document's browsing context.
    // If this navigable is a traversable navigable, then its active browsing context will be a top-level browsing context.
    if (auto document = active_document())
        return document->browsing_context();
    return nullptr;
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#nav-wp
GC::Ptr<HTML::WindowProxy> Navigable::active_window_proxy()
{
    // A navigable's active WindowProxy is its active browsing context's associated WindowProxy.
    if (auto browsing_context = active_browsing_context())
        return browsing_context->window_proxy();
    return nullptr;
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#nav-window
GC::Ptr<HTML::Window> Navigable::active_window()
{
    // A navigable's active window is its active WindowProxy's [[Window]].
    if (auto window_proxy = active_window_proxy())
        return window_proxy->window();
    return nullptr;
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#nav-target
String Navigable::target_name() const
{
    // A navigable's target name is its active session history entry's document state's navigable target name.
    return active_session_history_entry()->document_state()->navigable_target_name();
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#nav-container
GC::Ptr<NavigableContainer> Navigable::container() const
{
    // The container of a navigable navigable is the navigable container whose nested navigable is navigable, or null if there is no such element.
    return NavigableContainer::navigable_container_with_content_navigable(const_cast<Navigable&>(*this));
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#nav-container-document
GC::Ptr<DOM::Document> Navigable::container_document() const
{
    auto container = this->container();

    // 1. If navigable's container is null, then return null.
    if (!container)
        return nullptr;

    // 2. Return navigable's container's node document.
    return container->document();
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#nav-traversable
GC::Ptr<TraversableNavigable> Navigable::traversable_navigable() const
{
    // 1. Let navigable be inputNavigable.
    auto navigable = const_cast<Navigable*>(this);

    // 2. While navigable is not a traversable navigable, set navigable to navigable's parent.
    while (navigable && !is<TraversableNavigable>(*navigable))
        navigable = navigable->parent();

    // 3. Return navigable.
    return static_cast<TraversableNavigable*>(navigable);
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#nav-top
GC::Ptr<TraversableNavigable> Navigable::top_level_traversable()
{
    // 1. Let navigable be inputNavigable.
    auto navigable = this;

    // 2. While navigable's parent is not null, set navigable to navigable's parent.
    while (navigable->parent())
        navigable = navigable->parent();

    // 3. Return navigable.
    return as<TraversableNavigable>(navigable);
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#set-the-ongoing-navigation
void Navigable::set_ongoing_navigation(Variant<Empty, Traversal, String> ongoing_navigation, NavigationAPIAbortBehavior navigation_api_abort_behavior)
{
    // 1. If navigable's ongoing navigation is equal to newValue, then return.
    if (m_ongoing_navigation == ongoing_navigation)
        return;

    // 2. Inform the navigation API about aborting navigation given navigable.
    if (navigation_api_abort_behavior == NavigationAPIAbortBehavior::Abort)
        inform_the_navigation_api_about_aborting_navigation();

    // 3. Set navigable's ongoing navigation to newValue.
    auto was_traversal = m_ongoing_navigation.has<Traversal>();
    m_ongoing_navigation = ongoing_navigation;

    for (auto& navigation_observer : m_navigation_observers) {
        if (navigation_observer.ongoing_navigation_changed())
            navigation_observer.ongoing_navigation_changed()->function()();
    }

    // AD-HOC: If we just finished a traversal and there are navigations that were deferred because the traversal was
    //         ongoing, process them now. A freshly-created child navigable can also have pending navigations while
    //         its initial session history entry is being installed, so only drain once both gates are open.
    if (was_traversal && !ongoing_navigation.has<Traversal>() && m_has_session_history_entry_and_ready_for_navigation)
        process_pending_navigations();
}

void Navigable::queue_pending_navigation(NavigateParams params, PendingNavigationBehavior behavior)
{
    if (behavior == PendingNavigationBehavior::Replace)
        m_pending_navigations.clear();
    m_pending_navigations.append(move(params));
}

void Navigable::process_pending_navigations()
{
    while (!m_pending_navigations.is_empty()) {
        auto navigation_params = m_pending_navigations.take_first();
        begin_navigation(navigation_params);
    }
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#the-rules-for-choosing-a-navigable
Navigable::ChosenNavigable Navigable::choose_a_navigable(StringView name, TokenizedFeature::NoOpener no_opener, ActivateTab activate_tab, Optional<TokenizedFeature::Map const&> window_features)
{
    // 1. Let chosen be null.
    GC::Ptr<Navigable> chosen = nullptr;

    // 2. Let windowType be "existing or none".
    auto window_type = WindowType::ExistingOrNone;

    // 3. Let sandboxingFlagSet be currentNavigable's active document's active sandboxing flag set.
    auto sandboxing_flag_set = active_document()->active_sandboxing_flag_set();

    // 4. If name is the empty string or an ASCII case-insensitive match for "_self", then set chosen to currentNavigable.
    if (name.is_empty() || name.equals_ignoring_ascii_case("_self"sv)) {
        chosen = this;
    }

    // 5. Otherwise, if name is an ASCII case-insensitive match for "_parent",
    //    set chosen to currentNavigable's parent, if any, and currentNavigable otherwise.
    else if (name.equals_ignoring_ascii_case("_parent"sv)) {
        if (auto parent = this->parent())
            chosen = parent;
        else
            chosen = this;
    }

    // 6. Otherwise, if name is an ASCII case-insensitive match for "_top",
    //    set chosen to currentNavigable's traversable navigable.
    else if (name.equals_ignoring_ascii_case("_top"sv)) {
        chosen = traversable_navigable();
    }

    // 7. Otherwise, if name is not an ASCII case-insensitive match for "_blank" and noopener is false, then set chosen
    //    to the result of finding a navigable by target name given name and currentNavigable.
    else if (!name.equals_ignoring_ascii_case("_blank"sv) && no_opener == TokenizedFeature::NoOpener::No) {
        chosen = find_a_navigable_by_target_name(name);
    }

    // 8. If chosen is null, then a new top-level traversable is being requested, and what happens depends on the user
    //    agent's configuration and abilities — it is determined by the rules given for the first applicable option
    //    from the following list:
    if (!chosen) {
        // --> If currentNavigable's active window does not have transient activation and the user agent has been configured to
        //     not show popups (i.e., the user agent has a "popup blocker" enabled)
        if (active_window() && !active_window()->has_transient_activation() && traversable_navigable()->page().should_block_pop_ups()) {
            // FIXME: The user agent may inform the user that a popup has been blocked.
            dbgln("Pop-up blocked!");
        }

        // --> If sandboxingFlagSet has the sandboxed auxiliary navigation browsing context flag set
        else if (has_flag(sandboxing_flag_set, SandboxingFlagSet::SandboxedAuxiliaryNavigation)) {
            // FIXME: The user agent may report to a developer console that a popup has been blocked.
            dbgln("Pop-up blocked!");
        }

        // --> If the user agent has been configured such that in this instance it will create a new top-level traversable
        else if (true) { // FIXME: When is this the case?
            // 1. Consume user activation of currentNavigable's active window.
            active_window()->consume_user_activation();

            // 2. Set windowType to "new and unrestricted".
            window_type = WindowType::NewAndUnrestricted;

            // 3. Let currentDocument be currentNavigable's active document.
            auto current_document = active_document();

            // 4. If currentDocument's opener policy's value is "same-origin" or "same-origin-plus-COEP",
            //    and currentDocument's origin is not same origin with currentDocument's relevant settings object's top-level origin, then:
            if ((current_document->opener_policy().value == OpenerPolicyValue::SameOrigin || current_document->opener_policy().value == OpenerPolicyValue::SameOriginPlusCOEP)
                && !current_document->origin().is_same_origin(relevant_settings_object(*current_document).top_level_origin.value())) {

                // 1. Set noopener to true.
                no_opener = TokenizedFeature::NoOpener::Yes;

                // 2. Set name to "_blank".
                name = "_blank"sv;

                // 3. Set windowType to "new with no opener".
                window_type = WindowType::NewWithNoOpener;
            }
            // NOTE: In the presence of an opener policy,
            //       nested documents that are cross-origin with their top-level browsing context's active document always set noopener to true.

            // 5. Let targetName be the empty string.
            String target_name;

            // 6. If name is not an ASCII case-insensitive match for "_blank", then set targetName to name.
            if (!name.equals_ignoring_ascii_case("_blank"sv))
                target_name = MUST(String::from_utf8(name));

            auto create_new_traversable_closure = [this, no_opener, target_name, activate_tab, window_features](GC::Ptr<BrowsingContext> opener) -> GC::Ref<Navigable> {
                auto hints = WebViewHints::from_tokenised_features(window_features.value_or({}), traversable_navigable()->page());
                auto [page, window_handle] = traversable_navigable()->page().client().page_did_request_new_web_view(activate_tab, hints, no_opener);
                auto traversable = TraversableNavigable::create_a_new_top_level_traversable(*page, opener, target_name);
                page->set_top_level_traversable(traversable);
                traversable->set_window_handle(window_handle);
                return traversable;
            };
            auto create_new_traversable = GC::create_function(heap(), move(create_new_traversable_closure));

            // 7. If noopener is true, then set chosen to the result of creating a new top-level traversable given null and targetName.
            if (no_opener == TokenizedFeature::NoOpener::Yes) {
                chosen = create_new_traversable->function()(nullptr);
            }

            // 8. Otherwise:
            else {
                // 1. Set chosen to the result of creating a new top-level traversable given currentNavigable's active browsing context, targetName, and currentNavigable.
                // FIXME: "and currentNavigable", which is the openerNavigableForWebDriver parameter.
                chosen = create_new_traversable->function()(active_browsing_context());

                // 2. If sandboxingFlagSet's sandboxed navigation browsing context flag is set,
                //    then set chosen's active browsing context's one permitted sandboxed navigator to currentNavigable's active browsing context.
                if (has_flag(sandboxing_flag_set, SandboxingFlagSet::SandboxedNavigation))
                    chosen->active_browsing_context()->set_the_one_permitted_sandboxed_navigator(active_browsing_context());
            }

            // 9. If sandboxingFlagSet's sandbox propagates to auxiliary browsing contexts flag is set,
            //     then all the flags that are set in sandboxingFlagSet must be set in chosen's active browsing context's popup sandboxing flag set.
            if (has_flag(sandboxing_flag_set, SandboxingFlagSet::SandboxPropagatesToAuxiliaryBrowsingContexts))
                chosen->active_browsing_context()->set_popup_sandboxing_flag_set(chosen->active_browsing_context()->popup_sandboxing_flag_set() | sandboxing_flag_set);

            // 10. Set chosen's is created by web content to true.
            as<TraversableNavigable>(*chosen).set_is_created_by_web_content(true);
        }

        // --> If the user agent has been configured such that in this instance it will choose currentNavigable
        else if (false) { // FIXME: When is this the case?
            // Set chosen to current.
            chosen = *this;
        }

        // --> If the user agent has been configured such that in this instance it will not find a navigable
        else if (false) { // FIXME: When is this the case?
            // Do nothing.
        }
    }

    // 9. Return chosen and windowType
    return { chosen.ptr(), window_type };
}

// https://html.spec.whatwg.org/multipage/document-sequences.html#find-a-navigable-by-target-name
GC::Ptr<Navigable> Navigable::find_a_navigable_by_target_name(StringView name)
{
    // 1. Let currentDocument be currentNavigable's active document.
    auto& current_document = *active_document();

    // 2. Let sourceSnapshotParams be the result of snapshotting source snapshot params given currentDocument.
    auto source_snapshot_params = current_document.snapshot_source_snapshot_params();

    // 3. Let subtreesToSearch be an implementation-defined choice of one of the following:
    //    - « currentNavigable's traversable navigable, currentNavigable »
    //    - the inclusive ancestor navigables of currentDocument
    // FIXME: Decide which to use, or wait until the spec picks one.
    auto subtrees_to_search = current_document.inclusive_ancestor_navigables();

    // 4. For each subtreeToSearch of subtreesToSearch, in reverse order:
    for (auto const& subtree_to_search : subtrees_to_search.in_reverse()) {
        // 1. Let documentToSearch be subtreeToSearch's active document.
        auto& document_to_search = *subtree_to_search->active_document();

        // 2. For each navigable of the inclusive descendant navigables of documentToSearch:
        for (auto const& navigable : document_to_search.inclusive_descendant_navigables()) {
            // 1. If currentNavigable is not allowed by sandboxing to navigate navigable given sourceSnapshotParams, then optionally continue.
            if (!allowed_by_sandboxing_to_navigate(*navigable, source_snapshot_params))
                continue;

            // 2. If navigable's target name is name, then return navigable.
            if (navigable->target_name() == name)
                return *navigable;
        }
    }

    // 5. Let currentTopLevelBrowsingContext be currentNavigable's active browsing context's top-level browsing context.
    auto& current_top_level_browsing_context = *active_browsing_context()->top_level_browsing_context();

    // 6. Let group be currentTopLevelBrowsingContext's group.
    auto* group = current_top_level_browsing_context.group();

    // 7. For each topLevelBrowsingContext of group's browsing context set, in an implementation-defined order (the user agent should pick a consistent ordering, such as the most recently opened, most recently focused, or more closely related):
    for (auto const& top_level_browsing_context : group->browsing_context_set()) {
        // 1. If currentTopLevelBrowsingContext is topLevelBrowsingContext, then continue.
        if (&current_top_level_browsing_context == top_level_browsing_context)
            continue;

        // 2. Let documentToSearch be topLevelBrowsingContext's active document.
        auto* document_to_search = top_level_browsing_context->active_document();

        // 3. For each navigable of the inclusive descendant navigables of documentToSearch:
        for (auto const& navigable : document_to_search->inclusive_descendant_navigables()) {
            // 1. If currentNavigable's active browsing context is not familiar with navigable's active browsing context, then continue.
            if (!active_browsing_context()->is_familiar_with(*navigable->active_browsing_context()))
                continue;

            // 2. If currentNavigable is not allowed by sandboxing to navigate navigable given sourceSnapshotParams, then optionally continue.
            if (!allowed_by_sandboxing_to_navigate(*navigable, source_snapshot_params))
                continue;

            // 3. If navigable's target name is name, then return navigable.
            if (navigable->target_name() == name)
                return *navigable;
        }
    }

    // 8. Return null.
    return nullptr;
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-session-history-entries
Vector<NonnullRefPtr<SessionHistoryEntry>>& Navigable::get_session_history_entries() const
{
    // 1. Let traversable be navigable's traversable navigable.
    auto traversable = traversable_navigable();

    // FIXME: 2. Assert: this is running within traversable's session history traversal queue.

    // 3. If navigable is traversable, return traversable's session history entries.
    if (this == traversable)
        return traversable->session_history_entries();

    if (auto* entries = get_session_history_entries_if_present(*traversable, *this))
        return *entries;

    VERIFY_NOT_REACHED();
}

// https://html.spec.whatwg.org/multipage/browsers.html#determining-navigation-params-policy-container
static GC::Ref<PolicyContainer> determine_navigation_params_policy_container(URL::URL const& response_url,
    GC::Heap& heap,
    GC::Ptr<PolicyContainer> history_policy_container,
    GC::Ptr<PolicyContainer> initiator_policy_container,
    GC::Ptr<PolicyContainer> parent_policy_container,
    GC::Ptr<PolicyContainer> response_policy_container)
{
    // 1. If historyPolicyContainer is not null, then:
    if (history_policy_container) {
        // FIXME: 1. Assert: responseURL requires storing the policy container in history.

        // 2. Return a clone of historyPolicyContainer.
        return history_policy_container->clone(heap);
    }

    // 2. If responseURL is about:srcdoc, then:
    if (response_url == URL::about_srcdoc()) {
        // 1. Assert: parentPolicyContainer is not null.
        VERIFY(parent_policy_container);

        // 2. Return a clone of parentPolicyContainer.
        return parent_policy_container->clone(heap);
    }

    // 3. If responseURL is local and initiatorPolicyContainer is not null, then return a clone of initiatorPolicyContainer.
    if (Fetch::Infrastructure::is_local_url(response_url) && initiator_policy_container)
        return initiator_policy_container->clone(heap);

    // 4. If responsePolicyContainer is not null, then return responsePolicyContainer.
    // FIXME: File a spec issue to say "a clone of" here for consistency
    if (response_policy_container)
        return response_policy_container->clone(heap);

    // 5. Return a new policy container.
    return heap.allocate<PolicyContainer>(heap);
}

// https://html.spec.whatwg.org/multipage/browsers.html#obtain-coop
static OpenerPolicy obtain_an_opener_policy(GC::Ref<Fetch::Infrastructure::Response>, Fetch::Infrastructure::Request::ReservedClientType const& reserved_client)
{

    // 1. Let policy be a new opener policy.
    OpenerPolicy policy = {};

    // AD-HOC: We don't yet setup environments in all cases
    if (!reserved_client)
        return policy;

    auto& reserved_environment = *reserved_client;

    // 2. If reservedEnvironment is a non-secure context, then return policy.
    if (is_non_secure_context(reserved_environment))
        return policy;

    // FIXME: We don't yet have the technology to extract structured data from Fetch headers
    // FIXME: 3. Let parsedItem be the result of getting a structured field value given `Cross-Origin-Opener-Policy` and "item" from response's header list.
    // FIXME: 4. If parsedItem is not null, then:
    //     FIXME: nested steps...
    // FIXME: 5. Set parsedItem to the result of getting a structured field value given `Cross-Origin-Opener-Policy-Report-Only` and "item" from response's header list.
    // FIXME: 6. If parsedItem is not null, then:
    //     FIXME: nested steps...

    // 7. Return policy.
    return policy;
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#attempt-to-create-a-non-fetch-scheme-document
static GC::Ptr<DOM::Document> attempt_to_create_a_non_fetch_scheme_document(NonFetchSchemeNavigationParams const& params)
{
    // 1. Let url be navigationParams's URL.
    auto const& url = params.url;

    // 2. Let navigable be navigationParams's navigable.
    [[maybe_unused]] auto navigable = params.navigable;

    // 3. FIXME: If url is to be handled using a mechanism that does not affect navigable, e.g., because url's scheme is
    //    handled externally, then:
    if (false) {
        // 1. FIXME: Hand-off to external software given url, navigable, navigationParams's target snapshot sandboxing flags,
        //    navigationParams's source snapshot has transient activation, and navigationParams's initiator origin.

        // 2. Return null.
        return {};
    }

    // 4. FIXME: Handle url by displaying some sort of inline content, e.g., an error message because the specified scheme is
    //    not one of the supported protocols, or an inline prompt to allow the user to select a registered handler for
    //    the given scheme. Return the result of displaying the inline content given navigable, navigationParams's id,
    //    navigationParams's navigation timing type, and navigationParams's user involvement.

    dbgln("FIXME: Don't know how to navigate to {}", url);
    return {};
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#create-navigation-params-from-a-srcdoc-resource
static GC::Ref<NavigationParams> create_navigation_params_from_a_srcdoc_resource(
    Variant<Empty, String, POSTResource> const& document_resource,
    Optional<URL::Origin> const& origin,
    Variant<SerializedPolicyContainer, DocumentState::Client> const& history_policy_container_variant,
    Optional<URL::URL> const& about_base_url,
    GC::Ptr<Navigable> navigable,
    TargetSnapshotParams const& target_snapshot_params,
    UserNavigationInvolvement user_involvement,
    Optional<String> navigation_id)
{
    auto& vm = navigable->vm();
    VERIFY(navigable->active_window());
    auto& realm = navigable->active_window()->realm();

    // 1. Let documentResource be entry's document state's resource.
    VERIFY(document_resource.has<String>());

    // 2. Let response be a new response with
    //    URL: about:srcdoc
    //    header list: (`Content-Type`, `text/html`)
    //    body: the UTF-8 encoding of documentResource, as a body
    auto response = Fetch::Infrastructure::Response::create(vm);
    response->url_list().append(URL::about_srcdoc());
    response->header_list()->append({ "Content-Type"sv, "text/html"sv });
    response->set_body(Fetch::Infrastructure::byte_sequence_as_body(realm, document_resource.get<String>().bytes()));

    // 3. Let responseOrigin be the result of determining the origin given response's URL, targetSnapshotParams's sandboxing flags, and entry's document state's origin.
    auto response_origin = determine_the_origin(response->url(), target_snapshot_params.sandboxing_flags, origin);

    // 4. Let coop be a new opener policy.
    OpenerPolicy coop = {};

    // 5. Let coopEnforcementResult be a new opener policy enforcement result with
    //    url: response's URL
    //    origin: responseOrigin
    //    opener policy: coop
    OpenerPolicyEnforcementResult coop_enforcement_result {
        .url = *response->url(),
        .origin = response_origin,
        .opener_policy = coop
    };

    // 6. Let policyContainer be the result of determining navigation params policy container given response's URL,
    //    entry's document state's history policy container, null, navigable's container document's policy container, and null.
    GC::Ptr<PolicyContainer> history_policy_container = history_policy_container_variant.visit(
        [&](SerializedPolicyContainer const& s) -> GC::Ptr<PolicyContainer> { return create_a_policy_container_from_serialized_policy_container(realm.heap(), s); },
        [](DocumentState::Client) -> GC::Ptr<PolicyContainer> { return {}; });
    GC::Ptr<PolicyContainer> policy_container;
    if (navigable->container()) {
        // NOTE: Specification assumes that only navigables corresponding to iframes can be navigated to about:srcdoc.
        //       We also use srcdoc to implement load_html() for top level navigables so we need to null check container
        //       because it might be null.
        policy_container = determine_navigation_params_policy_container(*response->url(), realm.heap(), history_policy_container, {}, navigable->container_document()->policy_container(), {});
    } else {
        policy_container = realm.heap().allocate<PolicyContainer>(realm.heap());
    }

    // 7. Return a new navigation params, with
    //    id: navigationId
    //    navigable: navigable
    //    request: null
    //    response: response
    //    fetch controller: null
    //    commit early hints: null
    //    COOP enforcement result: coopEnforcementResult
    //    reserved environment: null
    //    origin: responseOrigin
    //    policy container: policyContainer
    //    final sandboxing flag set: targetSnapshotParams's sandboxing flags
    //    iframe element referrer policy: targetSnapshotParams's iframe element referrer policy
    //    opener policy: coop
    //    FIXME: navigation timing type: navTimingType
    //    about base URL: entry's document state's about base URL
    //    user involvement: userInvolvement
    return vm.heap().allocate<NavigationParams>(
        move(navigation_id),
        navigable,
        nullptr,
        response,
        nullptr,
        nullptr,
        move(coop_enforcement_result),
        nullptr,
        move(response_origin),
        *policy_container,
        target_snapshot_params.sandboxing_flags,
        target_snapshot_params.iframe_element_referrer_policy,
        move(coop),
        about_base_url,
        user_involvement);
}

static void perform_navigation_params_fetch(JS::Realm& realm, GC::Ref<NavigationParamsFetchStateHolder> state_holder, GC::Ref<GC::Function<void(GC::Ref<InternalNavigationResult>)>> top_level_completion_steps, GC::Ref<GC::Function<void()>> fetch_completion_steps)
{
    // 21. While true:
    // NOTE: To make this async, a loop is performed by calling "perform_navigation_params_fetch" again from within "perform_navigation_params_fetch",
    //       performs breaks by calling the passed in fetch completion steps and then returning and performs returns by
    //       calling the top level completion steps and then returning.

    // 1. If request's reserved client is not null and currentURL's origin is not the same as request's reserved client's creation URL's origin, then:
    if (state_holder->request->reserved_client() && !state_holder->current_url.origin().is_same_origin(state_holder->request->reserved_client()->creation_url.origin())) {
        // 1. Run the environment discarding steps for request's reserved client.
        state_holder->request->reserved_client()->discard_environment();

        // 2. Set request's reserved client to null.
        state_holder->request->set_reserved_client(nullptr);

        // 3. Set commitEarlyHints to null.
        state_holder->commit_early_hints = nullptr;
    }

    // 2. If request's reserved client is null, then:
    if (!state_holder->request->reserved_client()) {
        // 1. Let topLevelCreationURL be currentURL.
        Optional<URL::URL> top_level_creation_url = state_holder->current_url;

        // 2. Let topLevelOrigin be null.
        Optional<URL::Origin> top_level_origin;

        // 3. If navigable is not a top-level traversable, then:
        if (!state_holder->navigable->is_top_level_traversable()) {
            // 1. Let parentEnvironment be navigable's parent's active document's relevant settings object.
            auto parent = state_holder->navigable->parent();
            auto parent_document = parent ? parent->active_document() : nullptr;
            if (!parent || parent->has_been_destroyed() || !parent_document || parent_document->has_been_destroyed()) {
                // AD-HOC: A queued child navigation can resume after its parent document has been destroyed. The
                //         specification assumes the parent environment is still available here, but browser engines
                //         abandon this stale detached frame navigation instead of continuing it against a discarded
                //         parent.
                state_holder->response = Fetch::Infrastructure::Response::network_error(realm.vm(), "Parent document is no longer active"_string);
                fetch_completion_steps->function()();
                return;
            }
            auto& parent_environment = parent_document->relevant_settings_object();

            // 2. Set topLevelCreationURL to parentEnvironment's top-level creation URL.
            top_level_creation_url = parent_environment.top_level_creation_url;

            // 3. Set topLevelOrigin to parentEnvironment's top-level origin.
            top_level_origin = parent_environment.top_level_origin;
        }

        // 4. Set request's reserved client to a new environment whose id is a unique opaque string,
        //    target browsing context is navigable's active browsing context,
        //    creation URL is currentURL,
        //    top-level creation URL is topLevelCreationURL,
        //    and top-level origin is topLevelOrigin.
        // FIXME: Make this a proper unique opaque string.
        static int next_id = 1;
        auto id_string = MUST(String::formatted("create-by-fetching-{}", next_id++));
        state_holder->request->set_reserved_client(realm.create<Environment>(id_string, state_holder->current_url, top_level_creation_url, top_level_origin, state_holder->navigable->active_browsing_context()));
    }

    // 3. If the result of should navigation request of type be blocked by Content Security Policy? given request and cspNavigationType is "Blocked", then set response to a network error and break. [CSP]
    if (ContentSecurityPolicy::should_navigation_request_of_type_be_blocked_by_content_security_policy(state_holder->request, state_holder->csp_navigation_type) == ContentSecurityPolicy::Directives::Directive::Result::Blocked) {
        state_holder->response = Fetch::Infrastructure::Response::network_error(realm.vm(), "Blocked by Content Security Policy"_string);
        fetch_completion_steps->function()();
        return;
    }

    // 4. Set response to null.
    state_holder->response = nullptr;

    // 5. If fetchController is null, then set fetchController to the result of fetching request,
    //    with processEarlyHintsResponse set to processEarlyHintsResponse as defined below, processResponse
    //    set to processResponse as defined below, and useParallelQueue set to true.
    if (!state_holder->fetch_controller) {
        // FIXME: Let processEarlyHintsResponse be the following algorithm given a response earlyResponse:

        // Let processResponse be the following algorithm given a response fetchedResponse:
        auto process_response = [state_holder](GC::Ref<Fetch::Infrastructure::Response> fetch_response) {
            // 1. Set response to fetchedResponse.
            state_holder->response = fetch_response;
            VERIFY(state_holder->continuation_steps);
            state_holder->continuation_steps->function()(NavigationParamsFetchStateHolder::ContinuationReason::GotResponse);
        };

        state_holder->fetch_controller = Fetch::Fetching::fetch(
            realm,
            state_holder->request,
            Fetch::Infrastructure::FetchAlgorithms::create(realm.vm(),
                {
                    .process_request_body_chunk_length = {},
                    .process_request_end_of_body = {},
                    .process_early_hints_response = {},
                    .process_response = move(process_response),
                    .process_response_end_of_body = {},
                    .process_response_consume_body = {},
                }),
            Fetch::Fetching::UseParallelQueue::Yes);
    }
    // 6. Otherwise, process the next manual redirect for fetchController.
    else {
        state_holder->fetch_controller->process_next_manual_redirect();
    }

    // 7. Wait until either response is non-null, or navigable's ongoing navigation changes to no longer equal navigationId.
    GC::Ptr<NavigationObserver> ongoing_navigation_changed_observer;
    if (state_holder->navigation_id.has_value()) {
        ongoing_navigation_changed_observer = realm.create<NavigationObserver>(realm, *state_holder->navigable);
        ongoing_navigation_changed_observer->set_ongoing_navigation_changed([state_holder] {
            VERIFY(state_holder->continuation_steps);
            state_holder->continuation_steps->function()(NavigationParamsFetchStateHolder::ContinuationReason::OngoingNavigationChanged);
        });
    }

    state_holder->continuation_steps = GC::create_function(realm.heap(), [&realm, state_holder, ongoing_navigation_changed_observer, top_level_completion_steps, fetch_completion_steps](NavigationParamsFetchStateHolder::ContinuationReason continuation_reason) {
        // If the latter condition occurs, then abort fetchController, and return. Otherwise, proceed onward.
        if (state_holder->navigation_id.has_value()) {
            VERIFY(ongoing_navigation_changed_observer);
            ongoing_navigation_changed_observer->set_ongoing_navigation_changed({});

            if (continuation_reason == NavigationParamsFetchStateHolder::ContinuationReason::OngoingNavigationChanged) {
                if (state_holder->navigable->ongoing_navigation() != *state_holder->navigation_id) {
                    state_holder->fetch_controller->abort(realm, {});
                    auto result = realm.heap().allocate<InternalNavigationResult>();
                    result->navigation_params = Navigable::NullOrError {};
                    top_level_completion_steps->function()(*result);
                    return;
                }
            }
        }

        // 8. If request's body is null, then set entry's document state's resource to null.
        if (state_holder->request->body().has<Empty>()) {
            state_holder->resource_cleared = true;
            state_holder->resource = Empty {};
        }

        // 9. Set responsePolicyContainer to the result of creating a policy container from a fetch response given response and request's reserved client.
        state_holder->response_policy_container = create_a_policy_container_from_a_fetch_response(realm.heap(), *state_holder->response, state_holder->request->reserved_client());

        // 10. Set finalSandboxFlags to the union of targetSnapshotParams's sandboxing flags and responsePolicyContainer's CSP list's CSP-derived sandboxing flags.
        state_holder->final_sandbox_flags = state_holder->target_snapshot_params.sandboxing_flags | state_holder->response_policy_container->csp_list->csp_derived_sandboxing_flags();

        // 11. Set responseOrigin to the result of determining the origin given response's URL, finalSandboxFlags, and entry's document state's initiator origin.
        state_holder->response_origin = determine_the_origin(state_holder->response->url(), state_holder->final_sandbox_flags, state_holder->initiator_origin);

        // 12. If navigable is a top-level traversable, then:
        if (state_holder->navigable->is_top_level_traversable()) {
            // 1. Set responseCOOP to the result of obtaining an opener policy given response and request's reserved client.
            state_holder->response_coop = obtain_an_opener_policy(*state_holder->response, state_holder->request->reserved_client());

            // FIXME: 2. Set coopEnforcementResult to the result of enforcing the response's opener policy given navigable's active browsing context,
            //    response's URL, responseOrigin, responseCOOP, coopEnforcementResult and request's referrer.

            // FIXME: 3. If finalSandboxFlags is not empty and responseCOOP's value is not "unsafe-none", then set response to an appropriate network error and break.
            // NOTE: This results in a network error as one cannot simultaneously provide a clean slate to a response
            //       using opener policy and sandbox the result of navigating to that response.
        }

        // 13. FIXME: If response is not a network error, navigable is a child navigable, and the result of performing a cross-origin resource policy check
        //    with navigable's container document's origin, navigable's container document's relevant settings object, request's destination, response,
        //    and true is blocked, then set response to a network error and break.
        // NOTE: Here we're running the cross-origin resource policy check against the parent navigable rather than navigable itself
        //       This is because we care about the same-originness of the embedded content against the parent context, not the navigation source.

        // 14. Set locationURL to response's location URL given currentURL's fragment.
        state_holder->location_url = state_holder->response->location_url(state_holder->current_url.fragment());

        // 15. If locationURL is failure or null, then break.
        if (state_holder->location_url.is_error() || !state_holder->location_url.value().has_value()) {
            fetch_completion_steps->function()();
            return;
        }

        // 16. Assert: locationURL is a URL.
        // 17. Set entry's classic history API state to StructuredSerializeForStorage(null).
        state_holder->redirect_classic_history_api_state = MUST(structured_serialize_for_storage(realm.vm(), JS::js_null()));

        // 18. Let oldDocState be entry's document state.

        // 19. Set entry's document state to a new document state, with
        // history policy container: a clone of the oldDocState's history policy container if it is non-null; null otherwise
        // request referrer: oldDocState's request referrer
        // request referrer policy: oldDocState's request referrer policy
        // origin: oldDocState's origin
        // resource: oldDocState's resource
        // ever populated: oldDocState's ever populated
        // navigable target name: oldDocState's navigable target name
        auto new_doc_state = DocumentState::create();
        new_doc_state->set_history_policy_container(state_holder->history_policy_container);
        new_doc_state->set_request_referrer(state_holder->request_referrer);
        new_doc_state->set_request_referrer_policy(state_holder->request_referrer_policy);
        new_doc_state->set_origin(state_holder->origin);
        new_doc_state->set_resource(state_holder->resource);
        new_doc_state->set_ever_populated(state_holder->ever_populated);
        new_doc_state->set_navigable_target_name(state_holder->navigable_target_name);
        state_holder->replacement_document_state = new_doc_state;
        state_holder->initiator_origin = {};
        state_holder->about_base_url = {};

        // 20. If locationURL's scheme is not an HTTP(S) scheme, then:
        if (!Fetch::Infrastructure::is_http_or_https_scheme(state_holder->location_url.value()->scheme())) {
            // 1. Set entry's document state's resource to null.
            state_holder->replacement_document_state->set_resource(Empty {});

            // 2. Break.
            fetch_completion_steps->function()();
            return;
        }

        // 21. Set currentURL to locationURL.
        state_holder->current_url = state_holder->location_url.value().value();

        // 22. Set entry's URL to currentURL.
        state_holder->redirected_url = state_holder->current_url;

        perform_navigation_params_fetch(realm, state_holder, top_level_completion_steps, fetch_completion_steps);
    });
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#create-navigation-params-by-fetching
static void create_navigation_params_by_fetching(
    URL::URL url,
    Variant<Empty, String, POSTResource> document_resource,
    Fetch::Infrastructure::Request::ReferrerType request_referrer,
    ReferrerPolicy::ReferrerPolicy request_referrer_policy,
    Optional<URL::Origin> initiator_origin,
    Variant<SerializedPolicyContainer, DocumentState::Client> history_policy_container,
    Optional<URL::URL> about_base_url,
    Optional<URL::Origin> origin,
    String navigable_target_name,
    bool reload_pending,
    bool ever_populated,
    GC::Ptr<Navigable> navigable,
    GC::Ref<SourceSnapshotParams> source_snapshot_params,
    TargetSnapshotParams const& target_snapshot_params,
    ContentSecurityPolicy::Directives::Directive::NavigationType csp_navigation_type,
    UserNavigationInvolvement user_involvement,
    Optional<String> navigation_id,
    GC::Ref<GC::Function<void(GC::Ref<InternalNavigationResult>)>> completion_steps)
{
    auto& vm = navigable->vm();
    VERIFY(navigable->active_window());
    auto& realm = navigable->active_window()->realm();
    auto& active_document = *navigable->active_document();

    // FIXME: 1. Assert: this is running in parallel.

    // 2. Let documentResource be entry's document state's resource.
    // NOTE: documentResource is passed as a parameter.

    // 3. Let request be a new request, with
    //    url: entry's URL
    //    client: sourceSnapshotParams's fetch client
    //    destination: "document"
    //    credentials mode: "include"
    //    use-URL-credentials flag: set
    //    redirect mode: "manual"
    //    replaces client id: navigable's active document's relevant settings object's id
    //    mode: "navigate"
    //    referrer: entry's document state's request referrer
    //    referrer policy: entry's document state's request referrer policy
    //    policy container: sourceSnapshotParams's source policy container
    auto request = Fetch::Infrastructure::Request::create(vm);
    request->set_url(url);
    request->set_client(source_snapshot_params->fetch_client);
    request->set_destination(Fetch::Infrastructure::Request::Destination::Document);
    request->set_credentials_mode(Fetch::Infrastructure::Request::CredentialsMode::Include);
    request->set_use_url_credentials(true);
    request->set_redirect_mode(Fetch::Infrastructure::Request::RedirectMode::Manual);
    request->set_replaces_client_id(active_document.relevant_settings_object().id);
    request->set_mode(Fetch::Infrastructure::Request::Mode::Navigate);
    request->set_referrer(request_referrer);
    request->set_referrer_policy(request_referrer_policy);
    request->set_policy_container(source_snapshot_params->source_policy_container);

    // 4. If navigable is a top-level traversable, then set request's top-level navigation initiator origin to entry's
    //    document state's initiator origin.
    if (navigable->is_top_level_traversable())
        request->set_top_level_navigation_initiator_origin(initiator_origin);

    // 5. If request's client is null:
    if (request->client() == nullptr) {
        // Note: This only occurs in the case of a browser UI-initiated navigation.

        // 1. Set request's origin to a new opaque origin.
        request->set_origin(URL::Origin::create_opaque());

        // 2. Set request's service-workers mode to "all".
        request->set_service_workers_mode(Fetch::Infrastructure::Request::ServiceWorkersMode::All);

        // 3. Set request's referrer to "no-referrer".
        request->set_referrer(Fetch::Infrastructure::Request::Referrer::NoReferrer);
    }

    // 6. If documentResource is a POST resource:
    if (auto* post_resource = document_resource.get_pointer<POSTResource>()) {
        // 1. Set request's method to `POST`.
        request->set_method("POST"sv);

        // 2. Set request's body to documentResource's request body.
        request->set_body(document_resource.get<POSTResource>().request_body.value());

        // 3. Set `Content-Type` to documentResource's request content-type in request's header list.
        auto request_content_type = [&]() {
            switch (post_resource->request_content_type) {
            case POSTResource::RequestContentType::ApplicationXWWWFormUrlencoded:
                return "application/x-www-form-urlencoded"sv;
            case POSTResource::RequestContentType::MultipartFormData:
                return "multipart/form-data"sv;
            case POSTResource::RequestContentType::TextPlain:
                return "text/plain"sv;
            default:
                VERIFY_NOT_REACHED();
            }
        }();

        StringBuilder request_content_type_buffer;
        if (!post_resource->request_content_type_directives.is_empty()) {
            request_content_type_buffer.append(request_content_type);

            for (auto const& directive : post_resource->request_content_type_directives)
                request_content_type_buffer.appendff("; {}={}", directive.type, directive.value);

            request_content_type = request_content_type_buffer.string_view();
        }

        auto header = HTTP::Header::isomorphic_encode("Content-Type"sv, request_content_type);
        request->header_list()->append(move(header));
    }

    // 7. If entry's document state's reload pending is true, then set request's reload-navigation flag.
    if (reload_pending)
        request->set_reload_navigation(true);

    // 8. Otherwise, if entry's document state's ever populated is true, then set request's history-navigation flag.
    else if (ever_populated)
        request->set_history_navigation(true);

    // 9. If sourceSnapshotParams's has transient activation is true, then set request's user-activation to true.
    if (source_snapshot_params->has_transient_activation)
        request->set_user_activation(true);

    // 10. If navigable's container is non-null:
    if (navigable->container() != nullptr) {
        // 1. If the navigable's container has a browsing context scope origin, then set request's origin to that browsing context scope origin.
        // FIXME: From "browsing context scope origin": This definition is broken and needs investigation to see what it was intended to express: see issue #4703.
        //        The referenced issue suggests that it is a no-op to retrieve the browsing context scope origin.

        // 2. Set request's destination to navigable's container's local name.
        // FIXME: Are there other container types? If so, we need a helper here
        Web::Fetch::Infrastructure::Request::Destination destination = is<HTMLIFrameElement>(*navigable->container()) ? Web::Fetch::Infrastructure::Request::Destination::IFrame
                                                                                                                      : Web::Fetch::Infrastructure::Request::Destination::Object;
        request->set_destination(destination);

        // 3. If sourceSnapshotParams's fetch client is navigable's container document's relevant settings object,
        //    then set request's initiator type to navigable's container's local name.
        // NOTE: This ensure that only container-initiated navigations are reported to resource timing.
        if (source_snapshot_params->fetch_client == &navigable->container_document()->relevant_settings_object()) {
            // FIXME: Are there other container types? If so, we need a helper here
            Web::Fetch::Infrastructure::Request::InitiatorType initiator_type = is<HTMLIFrameElement>(*navigable->container()) ? Web::Fetch::Infrastructure::Request::InitiatorType::IFrame
                                                                                                                               : Web::Fetch::Infrastructure::Request::InitiatorType::Object;
            request->set_initiator_type(initiator_type);
        }
    }

    // NOTE: We use a heap-allocated cell to hold all the following state because the callbacks below will use them
    //       after this stack is freed.
    // 11. Let response be null.
    // 12. Let responseOrigin be null.
    // 13. Let fetchController be null.

    // 14. Let coopEnforcementResult be a new opener policy enforcement result, with
    // - url: navigable's active document's URL
    // - origin: navigable's active document's origin
    // - opener policy: navigable's active document's opener policy
    // - current context is navigation source: true if navigable's active document's origin is same origin with
    //                                         entry's document state's initiator origin otherwise false
    OpenerPolicyEnforcementResult coop_enforcement_result = {
        .url = active_document.url(),
        .origin = active_document.origin(),
        .opener_policy = active_document.opener_policy(),
        .current_context_is_navigation_source = initiator_origin.has_value() && active_document.origin().is_same_origin(*initiator_origin)
    };

    // 15. Let finalSandboxFlags be an empty sandboxing flag set.
    // 16. Let responsePolicyContainer be null.
    // 17. Let responseCOOP be a new opener policy.
    // 18. Let locationURL be null.
    // 19. Let currentURL be request's current URL.
    // 20. Let commitEarlyHints be null.
    // AD-HOC: Store required variables on the state holder to keep them alive whilst waiting on the fetch to complete.
    auto state_holder = realm.heap().allocate<NavigationParamsFetchStateHolder>(move(coop_enforcement_result), request->current_url(), request,
        move(initiator_origin), move(history_policy_container), move(about_base_url), source_snapshot_params,
        request_referrer, request_referrer_policy, move(origin), move(document_resource), ever_populated, move(navigable_target_name));
    state_holder->navigable = navigable;
    state_holder->csp_navigation_type = csp_navigation_type;
    state_holder->target_snapshot_params = target_snapshot_params;
    state_holder->navigation_id = move(navigation_id);

    perform_navigation_params_fetch(realm, state_holder, completion_steps, GC::create_function(realm.heap(), [&realm, state_holder, user_involvement, completion_steps] {
        auto result = realm.heap().allocate<InternalNavigationResult>();
        result->redirected_url = move(state_holder->redirected_url);
        result->classic_history_api_state = move(state_holder->redirect_classic_history_api_state);
        result->replacement_document_state = state_holder->replacement_document_state;
        result->resource_cleared = state_holder->resource_cleared;

        // 22. If locationURL is a URL whose scheme is not a fetch scheme, then return a new non-fetch scheme navigation params, with
        if (!state_holder->location_url.is_error() && state_holder->location_url.value().has_value() && !Fetch::Infrastructure::is_fetch_scheme(state_holder->location_url.value().value().scheme())) {
            // - id: navigationId
            // - navigable: navigable
            // - URL: locationURL
            // - target snapshot sandboxing flags: targetSnapshotParams's sandboxing flags
            // - source snapshot has transient activation: sourceSnapshotParams's has transient activation
            // - initiator origin: responseOrigin
            // FIXME: - navigation timing type: navTimingType
            // - user involvement: userInvolvement
            result->navigation_params = realm.heap().allocate<NonFetchSchemeNavigationParams>(
                state_holder->navigation_id,
                state_holder->navigable,
                state_holder->location_url.value().value(),
                state_holder->target_snapshot_params.sandboxing_flags,
                state_holder->source_snapshot_params->has_transient_activation,
                *state_holder->response_origin,
                user_involvement);
            completion_steps->function()(*result);
            return;
        }

        // 23. If any of the following are true:
        //       - response is a network error;
        //       - locationURL is failure; or
        //       - locationURL is a URL whose scheme is a fetch scheme
        //     then return null.
        if (state_holder->response->is_network_error()) {
            // AD-HOC: We pass the error message if we have one in NullWithError
            result->navigation_params = state_holder->response->network_error_message();
            completion_steps->function()(*result);
            return;
        }

        if (state_holder->location_url.is_error() || (state_holder->location_url.value().has_value() && Fetch::Infrastructure::is_fetch_scheme(state_holder->location_url.value().value().scheme()))) {
            result->navigation_params = Navigable::NullOrError {};
            completion_steps->function()(*result);
            return;
        }

        // 24. Assert: locationURL is null and response is not a network error.
        VERIFY(!state_holder->location_url.value().has_value());
        VERIFY(!state_holder->response->is_network_error());

        // 25. Let resultPolicyContainer be the result of determining navigation params policy container given response's URL,
        //     entry's document state's history policy container, sourceSnapshotParams's source policy container, null, and responsePolicyContainer.
        GC::Ptr<PolicyContainer> history_policy_container = state_holder->history_policy_container.visit(
            [&](SerializedPolicyContainer const& s) -> GC::Ptr<PolicyContainer> { return create_a_policy_container_from_serialized_policy_container(realm.heap(), s); },
            [](DocumentState::Client) -> GC::Ptr<PolicyContainer> { return {}; });
        auto result_policy_container = determine_navigation_params_policy_container(*state_holder->response->url(), realm.heap(), history_policy_container, state_holder->source_snapshot_params->source_policy_container, {}, state_holder->response_policy_container);

        // 26. If navigable's container is an iframe, and response's timing allow passed flag is set,
        //     then set navigable's container's pending resource-timing start time to null.
        if (state_holder->navigable->container() && state_holder->response->timing_allow_passed()) {
            if (auto* iframe_element = as_if<HTML::HTMLIFrameElement>(*state_holder->navigable->container()))
                iframe_element->set_pending_resource_start_time({});
        }

        // 27. Return a new navigation params, with
        //     id: navigationId
        //     navigable: navigable
        //     request: request
        //     response: response
        //     fetch controller: fetchController
        //     commit early hints: commitEarlyHints
        //     opener policy: responseCOOP
        //     reserved environment: request's reserved client
        //     origin: responseOrigin
        //     policy container: resultPolicyContainer
        //     final sandboxing flag set: finalSandboxFlags
        //     COOP enforcement result: coopEnforcementResult
        //     FIXME: navigation timing type: navTimingType
        //     about base URL: entry's document state's about base URL
        //     user involvement: userInvolvement
        // FIXME: Value for iframe element referrer policy is missing in the spec. https://github.com/whatwg/html/issues/12567
        //        So, we default to the empty string.
        result->navigation_params = realm.heap().allocate<NavigationParams>(
            state_holder->navigation_id,
            state_holder->navigable,
            state_holder->request,
            *state_holder->response,
            state_holder->fetch_controller,
            state_holder->commit_early_hints,
            state_holder->coop_enforcement_result,
            state_holder->request->reserved_client(),
            *state_holder->response_origin,
            result_policy_container,
            state_holder->final_sandbox_flags,
            ReferrerPolicy::ReferrerPolicy::EmptyString,
            state_holder->response_coop,
            state_holder->about_base_url,
            user_involvement);
        completion_steps->function()(*result);
    }));
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#populating-a-session-history-entry
void Navigable::populate_session_history_entry_document(
    URL::URL url,
    Variant<Empty, String, POSTResource> document_resource,
    Fetch::Infrastructure::Request::ReferrerType request_referrer,
    ReferrerPolicy::ReferrerPolicy request_referrer_policy,
    Optional<URL::Origin> initiator_origin,
    Optional<URL::Origin> origin,
    Variant<SerializedPolicyContainer, DocumentState::Client> history_policy_container,
    Optional<URL::URL> about_base_url,
    String navigable_target_name,
    bool reload_pending,
    bool ever_populated,
    GC::Ref<SourceSnapshotParams> source_snapshot_params,
    TargetSnapshotParams const& target_snapshot_params,
    UserNavigationInvolvement user_involvement,
    Optional<String> navigation_id,
    NavigationParamsVariant navigation_params,
    ContentSecurityPolicy::Directives::Directive::NavigationType csp_navigation_type,
    bool allow_POST,
    GC::Ptr<GC::Function<void(GC::Ptr<PopulateSessionHistoryEntryDocumentOutput>)>> completion_steps)
{
    // AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window.
    if (!active_window())
        return;

    // FIXME: 1. Assert: this is running in parallel.

    // 2. Assert: if navigationParams is non-null, then navigationParams's response is non-null.
    if (!navigation_params.has<NullOrError>())
        VERIFY(navigation_params.has<GC::Ref<NavigationParams>>() && navigation_params.get<GC::Ref<NavigationParams>>()->response);

    // 3. Let documentResource be entry's document state's resource.
    // NOTE: documentResource is passed as a parameter.

    auto received_navigation_params = GC::create_function(heap(), [this, url, navigation_id, user_involvement, completion_steps, csp_navigation_type](GC::Ref<InternalNavigationResult> result) {
        // AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window.
        if (!active_window())
            return;

        // 5. Queue a global task on the navigation and traversal task source, given navigable's active window, to run these steps:
        queue_global_task(Task::Source::NavigationAndTraversal, *active_window(), GC::create_function(heap(), [this, url, result, navigation_id, user_involvement, completion_steps, csp_navigation_type]() mutable {
            auto& navigation_params = result->navigation_params;

            // 1. If navigable's ongoing navigation no longer equals navigationId, then run completionSteps and abort these steps.
            if (navigation_id.has_value() && ongoing_navigation() != navigation_id) {
                if (completion_steps) {
                    completion_steps->function()(nullptr);
                }
                return;
            }

            auto output = heap().allocate<PopulateSessionHistoryEntryDocumentOutput>();
            output->redirected_url = move(result->redirected_url);
            output->classic_history_api_state = move(result->classic_history_api_state);
            output->replacement_document_state = result->replacement_document_state;
            output->resource_cleared = result->resource_cleared;

            // 2. Let saveExtraDocumentState be true.
            output->save_extra_document_state = true;

            // 3. If navigationParams is a non-fetch scheme navigation params, then:
            if (navigation_params.has<GC::Ref<NonFetchSchemeNavigationParams>>()) {
                // 1. Set entry's document state's document to the result of running attempt to create a non-fetch scheme
                //    document given navigationParams.
                //    NOTE: This can result in setting entry's document state's document to null, e.g., when handing-off to
                //    external software.
                output->document = attempt_to_create_a_non_fetch_scheme_document(navigation_params.get<GC::Ref<NonFetchSchemeNavigationParams>>());

                // 2. Set saveExtraDocumentState to false.
                output->save_extra_document_state = false;
            }

            // 4. Otherwise, if any of the following are true:
            //  - navigationParams is null;
            //  - the result of should navigation response to navigation request of type in target be blocked by Content Security Policy? given navigationParams's request, navigationParams's response, navigationParams's policy container's CSP list, cspNavigationType, and navigable is "Blocked";
            //  - FIXME: navigationParams's reserved environment is non-null and the result of checking a navigation response's adherence to its embedder policy given navigationParams's response, navigable, and navigationParams's policy container's embedder policy is false; or
            //  - the result of checking a navigation response's adherence to `X-Frame-Options` given navigationParams's response, navigable, navigationParams's policy container's CSP list, and navigationParams's origin is false,
            //    then:
            else if (navigation_params.visit(
                         [](NullOrError) { return true; },
                         [this, csp_navigation_type](GC::Ref<NavigationParams> navigation_params) {
                             auto csp_result = ContentSecurityPolicy::should_navigation_response_to_navigation_request_of_type_in_target_be_blocked_by_content_security_policy(navigation_params->request, *navigation_params->response, navigation_params->policy_container->csp_list, csp_navigation_type, *this);
                             if (csp_result == ContentSecurityPolicy::Directives::Directive::Result::Blocked)
                                 return true;

                             // FIXME: Pass in navigationParams's policy container's CSP list
                             return !check_a_navigation_responses_adherence_to_x_frame_options(navigation_params->response, this, navigation_params->policy_container->csp_list, navigation_params->origin);
                         },
                         [](GC::Ref<NonFetchSchemeNavigationParams>) { return false; })) {
                // 1. Set entry's document state's document to the result of creating a document for inline content that doesn't have a DOM, given navigable, null, navTimingType, and userInvolvement. The inline content should indicate to the user the sort of error that occurred.
                auto error_message = navigation_params.has<NullOrError>() ? navigation_params.get<NullOrError>().value_or("Unknown error"_string) : "The request was denied."_string;

                auto error_url = result->redirected_url.value_or(url);
                auto error_html = load_error_page(error_url, error_message).release_value_but_fixme_should_propagate_errors();
                output->document = create_document_for_inline_content(this, navigation_id, user_involvement, [this, error_html](auto& document) {
                    auto scripting_mode = document.is_scripting_enabled() ? HTML::ParserScriptingMode::Normal : HTML::ParserScriptingMode::Disabled;
                    auto parser = HTMLParser::create(document, error_html, scripting_mode, "utf-8"sv);
                    document.set_url(URL::about_error());
                    parser->run();

                    // FIXME: This should go in create_document_for_inline_content() instead.
                    // FIXME: Directly calling parser->the_end results in a deadlock, because it waits for the warning image to load.
                    //        However the response is never processed when parser->the_end is called.
                    //        Queuing a global task is a workaround for now.
                    queue_a_task(Task::Source::Unspecified, HTML::main_thread_event_loop(), document, GC::create_function(heap(), [&document]() {
                        HTMLParser::the_end(document);
                    }));
                });

                // 2. Make document unsalvageable given entry's document state's document and "navigation-failure".
                if (output->document)
                    output->document->make_unsalvageable("navigation-failure"_string);

                // 3. Set saveExtraDocumentState to false.
                output->save_extra_document_state = false;

                // 4. If navigationParams is not null, then:
                if (!navigation_params.has<NullOrError>()) {
                    // 1. Run the environment discarding steps for navigationParams's reserved environment.
                    navigation_params.visit(
                        [](GC::Ref<NavigationParams> const& it) {
                            it->reserved_environment->discard_environment();
                        },
                        [](auto const&) {});

                    // FIXME: 2. Invoke WebDriver BiDi navigation failed with navigable and a new WebDriver BiDi navigation status whose id is navigationId, status is "canceled", and url is navigationParams's response's URL.
                }
            }

            // FIXME: 5. Otherwise, if navigationParams's response has a `Content-Disposition` header specifying the attachment
            //    disposition type, then:
            else if (false) {
            }

            // 6. Otherwise, if navigationParams's response's status is not 204 and is not 205, then set entry's document state's document to the result of
            //    loading a document given navigationParams, sourceSnapshotParams, and entry's document state's initiator origin.
            else if (auto const& response = navigation_params.get<GC::Ref<NavigationParams>>()->response; response->status() != 204 && response->status() != 205) {
                auto nav_params = navigation_params.get<GC::Ref<NavigationParams>>();
                auto body = nav_params->response->body();

                // Get sniff bytes for MIME type detection. For streaming responses where bytes
                // haven't arrived yet, we must wait asynchronously.
                auto sniff_bytes = body ? body->sniff_bytes_if_available() : Optional<ReadonlyBytes> { ReadonlyBytes {} };
                if (!sniff_bytes.has_value()) {
                    // Async path: bytes not yet available, wait for them
                    body->wait_for_sniff_bytes(GC::create_function(heap(),
                        [output, nav_params, navigation_params, completion_steps](ReadonlyBytes sniff_bytes) {
                            // AD-HOC: The document may have been destroyed between when the fetch started and when the
                            //         bytes arrived.
                            if (nav_params->navigable->active_browsing_context())
                                output->document = load_document(nav_params, sniff_bytes);
                            output->navigation_params = navigation_params;
                            if (completion_steps)
                                completion_steps->function()(output);
                        }));
                    return;
                }

                // Sync path: bytes available immediately
                output->document = load_document(nav_params, sniff_bytes.value());
            }

            output->navigation_params = navigation_params;
            if (completion_steps)
                completion_steps->function()(output);
        }));
    });

    // Helper to wrap a NavigationParamsVariant in an InternalNavigationResult with no redirect mutations.
    auto wrap_navigation_params = [&](NavigationParamsVariant navigation_params) {
        auto result = heap().allocate<InternalNavigationResult>();
        result->navigation_params = move(navigation_params);
        received_navigation_params->function()(*result);
    };

    // 4. If navigationParams is null, then:
    if (navigation_params.has<NullOrError>()) {
        // 1. If documentResource is a string, then set navigationParams to the result of creating navigation params
        //    from a srcdoc resource given entry, navigable, targetSnapshotParams, userInvolvement, navigationId, and
        //    navTimingType.
        if (document_resource.has<String>()) {
            wrap_navigation_params(create_navigation_params_from_a_srcdoc_resource(
                document_resource,
                origin,
                history_policy_container,
                about_base_url,
                this, target_snapshot_params, user_involvement, navigation_id));
        }
        // 2. Otherwise, if all of the following are true:
        //    - entry's URL's scheme is a fetch scheme; and
        //    - documentResource is null, or allowPOST is true and documentResource's request body is not failure,
        //      (FIXME: check if request body is not failure)
        //    then set navigationParams to the result of creating navigation params by fetching given entry, navigable,
        //    sourceSnapshotParams, targetSnapshotParams, cspNavigationType, userInvolvement, navigationId, and
        //    navTimingType.
        else if (Fetch::Infrastructure::is_fetch_scheme(url.scheme()) && (document_resource.has<Empty>() || allow_POST)) {
            create_navigation_params_by_fetching(
                url,
                document_resource,
                request_referrer,
                request_referrer_policy,
                initiator_origin,
                history_policy_container,
                about_base_url,
                origin,
                navigable_target_name,
                reload_pending,
                ever_populated,
                this,
                source_snapshot_params,
                target_snapshot_params,
                csp_navigation_type,
                user_involvement,
                navigation_id,
                received_navigation_params);
        }
        // 3. Otherwise, if entry's URL's scheme is not a fetch scheme, then set navigationParams to a new non-fetch
        //    scheme navigation params, with:
        else if (!Fetch::Infrastructure::is_fetch_scheme(url.scheme())) {
            // - id: navigationId
            // - navigable: navigable
            // - URL: entry's URL
            // - target snapshot sandboxing flags: targetSnapshotParams's sandboxing flags
            // - source snapshot has transient activation: sourceSnapshotParams's has transient activation
            // - initiator origin: entry's document state's initiator origin
            // FIXME: - navigation timing type: navTimingType
            // - user involvement: userInvolvement
            wrap_navigation_params(vm().heap().allocate<NonFetchSchemeNavigationParams>(
                navigation_id,
                this,
                url,
                target_snapshot_params.sandboxing_flags,
                source_snapshot_params->has_transient_activation,
                *initiator_origin,
                user_involvement));
        }
    } else {
        wrap_navigation_params(move(navigation_params));
    }
}

static Bindings::NavigationHistoryBehavior determine_history_handling_for_navigation(Bindings::NavigationHistoryBehavior history_handling, URL::URL const& url, DOM::Document const& active_document, URL::Origin const& initiator_origin_snapshot)
{
    // 12. If historyHandling is "auto", then:
    if (history_handling == Bindings::NavigationHistoryBehavior::Auto) {
        // NB: The spec says "targetNavigable" here, but this algorithm has a "navigable".
        // 1. If url equals navigable's active document's URL,
        //     and initiatorOriginSnapshot is same origin with targetNavigable's active document's origin,
        //     then set historyHandling to "replace".
        if (url == active_document.url() && initiator_origin_snapshot.is_same_origin(active_document.origin()))
            history_handling = Bindings::NavigationHistoryBehavior::Replace;

        // 2. Otherwise, set historyHandling to "push".
        else
            history_handling = Bindings::NavigationHistoryBehavior::Push;
    }

    // 13. If the navigation must be a replace given url and navigable's active document, then set historyHandling to
    //     "replace".
    if (navigation_must_be_a_replace(url, active_document))
        history_handling = Bindings::NavigationHistoryBehavior::Replace;

    return history_handling;
}

WebIDL::ExceptionOr<void> Navigable::navigate(NavigateParams params)
{
    // AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window.
    if (!active_window())
        return {};

    auto source_document = params.source_document;
    auto exceptions_enabled = params.exceptions_enabled;

    auto& active_document = *this->active_document();
    auto& realm = active_document.realm();

    // 2. Let sourceSnapshotParams be the result of snapshotting source snapshot params given sourceDocument.
    auto source_snapshot_params = source_document->snapshot_source_snapshot_params();

    // 3. Let initiatorOriginSnapshot be sourceDocument's origin.
    auto initiator_origin_snapshot = source_document->origin();

    // 4. Let initiatorBaseURLSnapshot be sourceDocument's document base URL.
    auto initiator_base_url_snapshot = source_document->base_url();

    // 5. If sourceDocument's node navigable is not allowed by sandboxing to navigate navigable given sourceSnapshotParams, then:
    if (!source_document->navigable()->allowed_by_sandboxing_to_navigate(*this, source_snapshot_params)) {
        // 1. If exceptionsEnabled is true, then throw a "SecurityError" DOMException.
        if (exceptions_enabled) {
            return WebIDL::SecurityError::create(realm, "Source document's node navigable is not allowed to navigate"_utf16);
        }

        // 2 Return.
        return {};
    }

    if (!m_has_session_history_entry_and_ready_for_navigation) {
        queue_pending_navigation(move(params), PendingNavigationBehavior::Append);
        return {};
    }

    begin_navigation(move(params));
    return {};
}

// To navigate a navigable navigable to a URL url using an optional Document-or-null sourceDocument (default null),
// with an optional POST resource, string, or null documentResource (default null),
// an optional response-or-null response (default null), an optional boolean exceptionsEnabled (default false),
// an optional NavigationHistoryBehavior historyHandling (default "auto"),
// an optional serialized state-or-null navigationAPIState (default null),
// an optional entry list or null formDataEntryList (default null),
// an optional referrer policy referrerPolicy (default the empty string),
// an optional user navigation involvement userInvolvement (default "none"),
// an optional Element sourceElement (default null),
// and an optional boolean initialInsertion (default false):

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate

// Test-only: armed via Internals.clobberNextNavigationWithATraversal(). Consumed by the next call to begin_navigation,
// which simulates a concurrent session-history traversal interrupting the unload check.
static bool s_clobber_next_navigation_with_a_traversal = false;
void Navigable::clobber_next_navigation_with_a_traversal_for_testing()
{
    s_clobber_next_navigation_with_a_traversal = true;
}

void Navigable::begin_navigation(NavigateParams params)
{
    // AD-HOC: Not in the spec but we should not navigate a navigable that has been destroyed.
    //         This can happen when a session history traversal step for creating a child navigable
    //         runs after the navigable has been destroyed (e.g. an iframe is removed before its
    //         post-connection steps finish processing). Without this check, we would call
    //         set_delaying_load_events(true) below, creating a DocumentLoadEventDelayer on the
    //         parent document that is never cleared.
    if (has_been_destroyed())
        return;

    // AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window.
    if (!active_window())
        return;

    auto url = params.url;
    auto source_document = params.source_document;
    auto document_resource = params.document_resource;
    auto response = params.response;
    auto history_handling = params.history_handling;
    auto navigation_api_state = params.navigation_api_state;
    auto referrer_policy = params.referrer_policy;
    auto user_involvement = params.user_involvement;
    auto source_element = params.source_element;
    auto initial_insertion = params.initial_insertion;
    auto& active_document = *this->active_document();
    auto& vm = this->vm();

    // 1. Let cspNavigationType be "form-submission" if formDataEntryList is non-null; otherwise "other".
    auto csp_navigation_type = params.form_data_entry_list.has_value() ? ContentSecurityPolicy::Directives::Directive::NavigationType::FormSubmission : ContentSecurityPolicy::Directives::Directive::NavigationType::Other;

    // 2. Let sourceSnapshotParams be the result of snapshotting source snapshot params given sourceDocument.
    auto source_snapshot_params = source_document->snapshot_source_snapshot_params();

    // 3. Let initiatorOriginSnapshot be a new opaque origin.
    auto initiator_origin_snapshot = URL::Origin::create_opaque();

    // 4. Let initiatorBaseURLSnapshot be about:blank.
    auto initiator_base_url_snapshot = URL::about_blank();

    // FIXME: 5. If sourceDocument is null:
    if (false) {
        // 1. Assert: userInvolvement is "browser UI".
        VERIFY(user_involvement == UserNavigationInvolvement::BrowserUI);

        // 2. If url's scheme is "javascript", then set initiatorOriginSnapshot to navigable's active document's origin.
        if (url.scheme() == "javascript"sv)
            initiator_origin_snapshot = active_document.origin();
    }
    // 6. Otherwise:
    else {
        // 1. Assert: userInvolvement is not "browser UI".
        // FIXME: We currently crash if we do this! Uncomment once other places are fixed to handle browser UI navigation.
        // VERIFY(user_involvement != UserNavigationInvolvement::BrowserUI);

        // 2. If sourceDocument's node navigable is not allowed by sandboxing to navigate navigable given sourceSnapshotParams:
        // NB: This step is handled in Navigable::navigate()

        // 3. Set initiatorOriginSnapshot to sourceDocument's origin.
        initiator_origin_snapshot = source_document->origin();

        // 4. Set initiatorBaseURLSnapshot to sourceDocument's document base URL.
        initiator_base_url_snapshot = source_document->base_url();
    }

    // 5. If sourceDocument's node navigable is not allowed by sandboxing to navigate navigable given sourceSnapshotParams, then:
    // NOTE: This step is handled in Navigable::navigate()

    // 7. Let navigationId be the result of generating a random UUID.
    auto navigation_id = Crypto::generate_random_uuid();

    // FIXME: 8. If the surrounding agent is equal to navigable's active document's relevant agent, then continue these steps.
    //           Otherwise, queue a global task on the navigation and traversal task source given navigable's active window to continue these steps.

    // 9. If navigable's active document's unload counter is greater than 0,
    //    then invoke WebDriver BiDi navigation failed with navigable and a WebDriver BiDi navigation status whose id
    //    is navigationId, status is "canceled", and url is url, and return.
    if (active_document.unload_counter() > 0) {
        // FIXME: invoke WebDriver BiDi navigation failed with navigable and a WebDriver BiDi navigation status whose id
        //        is navigationId, status is "canceled", and url is url
        return;
    }

    // 10. Let container be navigable's container.
    auto& container = m_container;

    // 11. If container is an iframe element and will lazy load element steps given container returns true,
    //     then stop intersection-observing a lazy loading element container and set container's lazy load resumption steps to null.
    if (container && container->is_html_iframe_element()) {
        auto& iframe_element = static_cast<HTMLIFrameElement&>(*container);
        if (iframe_element.will_lazy_load_element()) {
            iframe_element.document().stop_intersection_observing_a_lazy_loading_element(iframe_element);
            iframe_element.set_lazy_load_resumption_steps(nullptr);
        }
    }

    // 12-13. Determine historyHandling for this navigation.
    history_handling = determine_history_handling_for_navigation(history_handling, url, active_document, initiator_origin_snapshot);

    // 14. If all of the following are true:
    //     - documentResource is null;
    //     - response is null;
    //     - url equals navigable's active session history entry's URL with exclude fragments set to true; and
    //     - url's fragment is non-null,
    //     then:
    if (document_resource.has<Empty>()
        && !response
        && url.equals(active_session_history_entry()->url(), URL::ExcludeFragment::Yes)
        && url.fragment().has_value()) {
        // 1. Navigate to a fragment given navigable, url, historyHandling, userInvolvement, sourceElement, navigationAPIState, and navigationId.
        navigate_to_a_fragment(url, to_history_handling_behavior(history_handling), user_involvement, source_element, navigation_api_state, navigation_id);

        // 2. Return.
        return;
    }

    // 15. If navigable's parent is non-null, then set navigable's is delaying load events to true.
    if (parent() != nullptr)
        set_delaying_load_events(true);

    // 16. Let targetSnapshotParams be the result of snapshotting target snapshot params given navigable.
    auto target_snapshot_params = snapshot_target_snapshot_params();

    // FIXME: 17. Invoke WebDriver BiDi navigation started with navigable and a new WebDriver BiDi navigation status whose id
    //     is navigationId, status is "pending", and url is url.

    // 18. If navigable's ongoing navigation is "traversal", then:
    if (ongoing_navigation().has<Traversal>()) {
        // FIXME: 1. Invoke WebDriver BiDi navigation failed with navigable and a new WebDriver BiDi navigation status whose id
        //    is navigationId, status is "canceled", and url is url.

        // AD-HOC: The HTML Standard cancels a navigation that starts while a traversal is ongoing. We defer it
        //         instead so UI-initiated navigations that race the tail end of a previous load are not dropped.
        //         Match Chromium, WebKit, and Gecko's observable behavior by letting the newest navigation win.
        //         See https://github.com/whatwg/html/issues/12581.
        queue_pending_navigation(move(params), PendingNavigationBehavior::Replace);

        // 2. Return.
        return;
    }

    // 19. Set the ongoing navigation for navigable to navigationId.
    set_ongoing_navigation(navigation_id);

    // 20. If url's scheme is "javascript", then:
    if (url.scheme() == "javascript"sv) {
        // 1. Queue a global task on the navigation and traversal task source given navigable's active window to navigate to a javascript: URL given navigable, url, historyHandling, sourceSnapshotParams, initiatorOriginSnapshot, userInvolvement, cspNavigationType, initialInsertion, and navigationId.
        VERIFY(active_window());
        queue_global_task(Task::Source::NavigationAndTraversal, *active_window(), GC::create_function(heap(), [this, url, history_handling, source_snapshot_params, initiator_origin_snapshot, user_involvement, csp_navigation_type, initial_insertion, navigation_id] {
            navigate_to_a_javascript_url(url, to_history_handling_behavior(history_handling), source_snapshot_params, initiator_origin_snapshot, user_involvement, csp_navigation_type, initial_insertion, navigation_id);
        }));

        // 2. Return.
        return;
    }

    // 21. If all of the following are true:
    //     - userInvolvement is not "browser UI";
    //     - navigable's active document's origin is same origin-domain with sourceDocument's origin;
    //     - navigable's active document's is initial about:blank is false; and
    //     - url's scheme is a fetch scheme
    //     then:
    if (user_involvement != UserNavigationInvolvement::BrowserUI && active_document.origin().is_same_origin_domain(source_document->origin()) && !active_document.is_initial_about_blank() && Fetch::Infrastructure::is_fetch_scheme(url.scheme())) {
        // 1. Let navigation be navigable's active window's navigation API.
        VERIFY(active_window());
        auto navigation = active_window()->navigation();

        // 2. Let entryListForFiring be formDataEntryList if documentResource is a POST resource; otherwise, null.
        auto entry_list_for_firing = [&]() -> Optional<GC::ConservativeVector<XHR::FormDataEntry>> {
            if (document_resource.has<POSTResource>())
                return GC::ConservativeVector { params.form_data_entry_list.value() };
            return {};
        }();

        // 3. Let navigationAPIStateForFiring be navigationAPIState if navigationAPIState is not null;
        //    otherwise, StructuredSerializeForStorage(undefined).
        auto navigation_api_state_for_firing = navigation_api_state.value_or(MUST(structured_serialize_for_storage(vm, JS::js_undefined())));

        // 4. Let continue be the result of firing a push/replace/reload navigate event at navigation
        //    with navigationType set to historyHandling, isSameDocument set to false, userInvolvement set to userInvolvement,
        //    sourceElement set to sourceElement, formDataEntryList set to entryListForFiring, destinationURL set to url,
        //    and navigationAPIState set to navigationAPIStateForFiring.
        auto navigation_type = [](Bindings::NavigationHistoryBehavior history_handling) {
            switch (history_handling) {
            case Bindings::NavigationHistoryBehavior::Push:
                return Bindings::NavigationType::Push;
            case Bindings::NavigationHistoryBehavior::Replace:
                return Bindings::NavigationType::Replace;
            case Bindings::NavigationHistoryBehavior::Auto:
            default:
                VERIFY_NOT_REACHED();
            }
        }(history_handling);
        auto continue_ = navigation->fire_a_push_replace_reload_navigate_event(navigation_type, url, false, user_involvement, source_element, entry_list_for_firing, navigation_api_state_for_firing);

        // 5. If continue is false, then return.
        if (!continue_)
            return;
    }

    // FIXME: 22. If sourceDocument is navigable's container document, then reserve deferred fetch quota for navigable's container given url's origin.

    // 23. In parallel, run these steps:
    Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [this, source_snapshot_params, target_snapshot_params, csp_navigation_type, document_resource, url, navigation_id, referrer_policy, initiator_origin_snapshot, response, history_handling, initiator_base_url_snapshot, user_involvement, params = move(params)] mutable {
        // AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window.
        if (!active_window()) {
            set_delaying_load_events(false);
            return;
        }

        // 1. Let unloadPromptCanceled be the result of checking if unloading is user-canceled for navigable's active document's inclusive descendant navigables.
        traversable_navigable()->check_if_unloading_is_canceled(this->active_document()->inclusive_descendant_navigables(),
            GC::create_function(heap(), [this, source_snapshot_params, target_snapshot_params, csp_navigation_type, document_resource, url, navigation_id, referrer_policy, initiator_origin_snapshot, response, history_handling, initiator_base_url_snapshot, user_involvement, params = move(params)](TraversableNavigable::CheckIfUnloadingIsCanceledResult unload_prompt_canceled) mutable {
                // 2. If unloadPromptCanceled is not "continue", or navigable's ongoing navigation is no longer navigationId:
                if (unload_prompt_canceled != TraversableNavigable::CheckIfUnloadingIsCanceledResult::Continue) {
                    // FIXME: 1. Invoke WebDriver BiDi navigation failed with navigable and a new WebDriver BiDi navigation status whose id is navigationId, status is "canceled", and url is url.
                    if (is_top_level_traversable())
                        active_browsing_context()->page().client().page_did_cancel_loading(url);

                    // 2. Abort these steps.
                    set_delaying_load_events(false);
                    return;
                }

                // Test-only (Internals.clobberNextNavigationWithATraversal): re-stamp our ongoing navigation with a
                // synthetic traversal now, and clear it on a later turn (which drains deferred navigations). This
                // deterministically reproduces the race the guard below must survive.
                if (s_clobber_next_navigation_with_a_traversal) {
                    s_clobber_next_navigation_with_a_traversal = false;
                    set_ongoing_navigation(Traversal::Tag, NavigationAPIAbortBehavior::Preserve);
                    Platform::EventLoopPlugin::the().deferred_invoke(GC::create_function(heap(), [this] {
                        set_ongoing_navigation(Empty {}, NavigationAPIAbortBehavior::Preserve);
                    }));
                }

                if (ongoing_navigation() != navigation_id) {
                    // AD-HOC: If an ongoing traversal re-stamped our navigation ID while we were checking whether
                    //         unloading was canceled, this navigation wasn't actually superseded by a newer navigation.
                    //         Re-defer it (mirroring 66c54b129514), so it runs once the traversal completes — rather
                    //         than silently dropping it and leaving the navigable stuck with no load event ever firing.
                    //         See https://github.com/whatwg/html/issues/12581.
                    if (ongoing_navigation().has<Traversal>())
                        queue_pending_navigation(move(params), PendingNavigationBehavior::Append);
                    set_delaying_load_events(false);
                    return;
                }

                // AD-HOC: If we are not able to continue in this process, request a new process from the UI.
                if (is_top_level_traversable() && !active_browsing_context()->page().client().is_url_suitable_for_same_process_navigation(this->active_document()->url(), url)) {
                    active_browsing_context()->page().client().request_new_process_for_navigation(url, document_resource, history_handling);
                    set_delaying_load_events(false);
                    return;
                }

                // AD-HOC: Tell the UI that we started loading.
                if (is_top_level_traversable()) {
                    active_browsing_context()->page().client().page_did_start_loading(url, document_resource, false, history_handling);
                }

                // AD-HOC: Subsequent steps will fail if the navigable doesn't have an active window.
                if (!active_window()) {
                    set_delaying_load_events(false);
                    return;
                }

                // 3. Queue a global task on the navigation and traversal task source given navigable's active window to abort a document and its descendants given navigable's active document.
                queue_global_task(Task::Source::NavigationAndTraversal, *active_window(), GC::create_function(heap(), [this] {
                    this->active_document()->abort_a_document_and_its_descendants();
                }));

                // 4. Let documentState be a new document state with
                //    request referrer policy: referrerPolicy
                //    initiator origin: initiatorOriginSnapshot
                //    resource: documentResource
                //    navigable target name: navigable's target name
                auto document_state = DocumentState::create();
                document_state->set_request_referrer_policy(referrer_policy);
                document_state->set_initiator_origin(initiator_origin_snapshot);
                document_state->set_resource(document_resource);
                document_state->set_navigable_target_name(target_name());

                // 5. If url matches about:blank or is about:srcdoc, then:
                // FIXME: Is calling url_matches_about_srcdoc() correct? https://github.com/whatwg/html/issues/10900
                if (url_matches_about_blank(url) || url_matches_about_srcdoc(url)) {
                    // AD-HOC: document_resource cannot have an Empty if the url is about:srcdoc since we rely on document_resource
                    //         having a String to call create_navigation_params_from_a_srcdoc_resource
                    if (url_matches_about_srcdoc(url) && document_resource.has<Empty>()) {
                        document_state->set_resource({ String {} });
                    }
                    // 1. Set documentState's origin to initiatorOriginSnapshot.
                    document_state->set_origin(document_state->initiator_origin());

                    // 2. Set documentState's about base URL to initiatorBaseURLSnapshot.
                    document_state->set_about_base_url(initiator_base_url_snapshot);
                }

                // 6. Let historyEntry be a new session history entry, with its URL set to url and its document state set to documentState.
                auto history_entry = SessionHistoryEntry::create();
                history_entry->set_url(url);
                history_entry->set_document_state(document_state);

                // 7. Let navigationParams be null.
                NavigationParamsVariant navigation_params = Navigable::NullOrError {};

                // 8. If response is non-null:
                if (response) {
                    auto response_url = response->url();
                    VERIFY(response_url.has_value());

                    // 1. Let sourcePolicyContainer be a clone of the sourceDocument's policy container, if
                    //    sourceDocument is not null; otherwise null.
                    auto source_policy_container = source_snapshot_params->source_policy_container;

                    // 2. Let policyContainer be the result of determining navigation params policy container given
                    //    response's URL, null, sourcePolicyContainer, navigable's container document's policy container,
                    //    and null.
                    GC::Ptr<PolicyContainer> parent_policy_container;
                    if (auto container_document = this->container_document())
                        parent_policy_container = container_document->policy_container();
                    else if (*response_url == URL::about_srcdoc()) {
                        // NOTE: Specification assumes that only navigables corresponding to iframes can be navigated to about:srcdoc.
                        //       We also use srcdoc to implement load_html() for top level navigables so we need a policy container
                        //       because the navigable might not have a container.
                        parent_policy_container = heap().allocate<PolicyContainer>(heap());
                    }
                    auto policy_container = determine_navigation_params_policy_container(*response_url, heap(), {}, source_policy_container, parent_policy_container, {});

                    // 3. Let finalSandboxFlags be the union of targetSnapshotParams's sandboxing flags and
                    //    policyContainer's CSP list's CSP-derived sandboxing flags.
                    auto final_sandbox_flags = target_snapshot_params.sandboxing_flags | policy_container->csp_list->csp_derived_sandboxing_flags();

                    // 4. Let responseOrigin be the result of determining the origin given response's URL,
                    //    finalSandboxFlags, and documentState's initiator origin.
                    auto response_origin = determine_the_origin(response_url, final_sandbox_flags, document_state->initiator_origin());

                    // 5. Let coop be a new opener policy.
                    OpenerPolicy response_coop = {};

                    // 6. Let coopEnforcementResult be a new opener policy enforcement result with
                    //    url: response's URL
                    //    origin: responseOrigin
                    //    opener policy: coop
                    OpenerPolicyEnforcementResult coop_enforcement_result {
                        .url = *response_url,
                        .origin = response_origin,
                        .opener_policy = response_coop,
                    };

                    // 7. Set navigationParams to a new navigation params, with
                    //    id: navigationId
                    //    navigable: navigable
                    //    request: null
                    //    response: response
                    //    fetch controller: null
                    //    commit early hints: null
                    //    COOP enforcement result: coopEnforcementResult
                    //    reserved environment: null
                    //    origin: responseOrigin
                    //    policy container: policyContainer
                    //    final sandboxing flag set: finalSandboxFlags
                    //    iframe element referrer policy: targetSnapshotParams's iframe element referrer policy
                    //    opener policy: coop
                    //    FIXME: navigation timing type: "navigate"
                    //    about base URL: documentState's about base URL
                    //    user involvement: userInvolvement
                    navigation_params = heap().allocate<NavigationParams>(
                        navigation_id,
                        this,
                        nullptr,
                        response,
                        nullptr,
                        nullptr,
                        move(coop_enforcement_result),
                        nullptr,
                        move(response_origin),
                        policy_container,
                        final_sandbox_flags,
                        target_snapshot_params.iframe_element_referrer_policy,
                        response_coop,
                        document_state->about_base_url(),
                        user_involvement);
                }

                // 9. Attempt to populate the history entry's document for historyEntry, given navigable, "navigate",
                //    sourceSnapshotParams, targetSnapshotParams, userInvolvement, navigationId, navigationParams,
                //    cspNavigationType, with allowPOST set to true and completionSteps set to the following step:
                populate_session_history_entry_document(
                    history_entry->url(),
                    history_entry->document_state()->resource(),
                    history_entry->document_state()->request_referrer(),
                    history_entry->document_state()->request_referrer_policy(),
                    history_entry->document_state()->initiator_origin(),
                    history_entry->document_state()->origin(),
                    history_entry->document_state()->history_policy_container(),
                    history_entry->document_state()->about_base_url(),
                    history_entry->document_state()->navigable_target_name(),
                    history_entry->document_state()->reload_pending(),
                    history_entry->document_state()->ever_populated(),
                    source_snapshot_params, target_snapshot_params, user_involvement, navigation_id, navigation_params, csp_navigation_type, true, GC::create_function(heap(), [this, history_entry, history_handling, navigation_id, user_involvement](GC::Ptr<PopulateSessionHistoryEntryDocumentOutput> output) {
                        if (output)
                            output->apply_to(*history_entry);
                        auto pending_document = output ? output->document : GC::Ptr<DOM::Document> {};
                        // 1. Append session history traversal steps to navigable's traversable to finalize a cross-document navigation given navigable, historyHandling, userInvolvement, and historyEntry.
                        traversable_navigable()->append_session_history_traversal_steps(GC::create_function(heap(), [this, history_entry, history_handling, navigation_id, user_involvement, pending_document](NonnullRefPtr<Core::Promise<Empty>> signal) {
                            if (this->has_been_destroyed()) {
                                // AD-HOC: This check is not in the spec but we should not continue navigation if navigable has been destroyed.
                                set_delaying_load_events(false);
                                signal->resolve({});
                                return;
                            }
                            if (this->ongoing_navigation() != navigation_id) {
                                // AD-HOC: This check is not in the spec but we should not continue navigation if ongoing navigation id has changed.
                                set_delaying_load_events(false);
                                signal->resolve({});
                                return;
                            }
                            finalize_a_cross_document_navigation(*this, to_history_handling_behavior(history_handling), user_involvement, history_entry, pending_document, navigation_id, GC::create_function(heap(), [signal](HistoryStepResult) {
                                signal->resolve({});
                            }));
                        }));
                    }));
            }));
    }));
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate-fragid
void Navigable::navigate_to_a_fragment(URL::URL const& url, HistoryHandlingBehavior history_handling, UserNavigationInvolvement user_involvement, GC::Ptr<DOM::Element> source_element, Optional<SerializationRecord> navigation_api_state, String navigation_id)
{
    // 1. Let navigation be navigable's active window's navigation API.
    VERIFY(active_window());
    auto navigation = active_window()->navigation();

    // 2. Let destinationNavigationAPIState be navigable's active session history entry's navigation API state.
    // 3. If navigationAPIState is not null, then set destinationNavigationAPIState to navigationAPIState.
    auto destination_navigation_api_state = navigation_api_state.has_value() ? *navigation_api_state : active_session_history_entry()->navigation_api_state();

    // 4. Let continue be the result of firing a push/replace/reload navigate event at navigation with navigationType
    //    set to historyHandling, isSameDocument set to true, userInvolvement set to userInvolvement, sourceElement set
    //    to sourceElement, destinationURL set to url, and navigationAPIState set to destinationNavigationAPIState.
    auto navigation_type = history_handling == HistoryHandlingBehavior::Push ? Bindings::NavigationType::Push : Bindings::NavigationType::Replace;
    bool const continue_ = navigation->fire_a_push_replace_reload_navigate_event(navigation_type, url, true, user_involvement, source_element, {}, destination_navigation_api_state);

    // 5. If continue is false, then return.
    if (!continue_)
        return;

    save_persisted_state_to_active_session_history_entry();
    auto active_entry = active_session_history_entry();

    // 6. Let historyEntry be a new session history entry, with
    //      URL: url
    //      document state: navigable's active session history entry's document state
    //      navigation API state: destinationNavigationAPIState
    //      scroll restoration mode: navigable's active session history entry's scroll restoration mode
    auto history_entry = SessionHistoryEntry::create();
    history_entry->set_url(url);
    history_entry->set_document_state(active_entry->document_state());
    history_entry->set_navigation_api_state(destination_navigation_api_state);
    history_entry->set_scroll_restoration_mode(active_entry->scroll_restoration_mode());
    history_entry->set_scroll_position_data(active_entry->scroll_position_data());

    // 7. Let entryToReplace be navigable's active session history entry if historyHandling is "replace", otherwise null.
    auto entry_to_replace = history_handling == HistoryHandlingBehavior::Replace ? active_entry : nullptr;

    // 8. Let history be navigable's active document's history object.
    auto history = active_document()->history();

    // 9. Let scriptHistoryIndex be history's index.
    auto script_history_index = history->m_index;

    // 10. Let scriptHistoryLength be history's length.
    auto script_history_length = history->m_length;

    // 11. If historyHandling is "push", then:
    if (history_handling == HistoryHandlingBehavior::Push) {
        // 1. Set history's state to null.
        history->set_state(JS::js_null());

        // 2. Increment scriptHistoryIndex.
        script_history_index++;

        // 3. Set scriptHistoryLength to scriptHistoryIndex + 1.
        script_history_length = script_history_index + 1;
    }

    // 12. Set navigable's active session history entry to historyEntry.
    m_active_session_history_entry = history_entry;

    // 13. Update document for history step application given navigable's active document, historyEntry, true, scriptHistoryIndex, and scriptHistoryLength.
    // AD HOC: Skip updating the navigation api entries twice here
    active_document()->update_for_history_step_application(*history_entry, true, script_history_length, script_history_index, navigation_type, {}, {}, false);

    // 14. Update the navigation API entries for a same-document navigation given navigation, historyEntry, and historyHandling.
    navigation->update_the_navigation_api_entries_for_a_same_document_navigation(history_entry, navigation_type);

    // 15. Scroll to the fragment given navigable's active document.
    // FIXME: Specification doesn't say when document url needs to update during fragment navigation
    active_document()->set_url(url);
    active_document()->scroll_to_the_fragment();

    // 16. Let traversable be navigable's traversable navigable.
    auto traversable = traversable_navigable();

    // AD-HOC: Browser engines commit same-document navigations synchronously when no traversal state is active. Keep
    //         the spec's queued synchronous-navigation steps as the fallback for reentrant traversal work and child
    //         navigables whose nested history is not ready yet.
    // 17. Append the following session history synchronous navigation steps involving navigable to traversable:
    if (!traversable->try_to_synchronously_commit_same_document_navigation(*this, history_entry, entry_to_replace)) {
        traversable->append_session_history_synchronous_navigation_steps(*this, GC::create_function(heap(), [this, traversable, history_entry, entry_to_replace, navigation_id, history_handling, user_involvement](NonnullRefPtr<Core::Promise<Empty>> signal) {
            // 1. Finalize a same-document navigation given traversable, navigable, historyEntry, entryToReplace,
            //    historyHandling, and userInvolvement.
            finalize_a_same_document_navigation(*traversable, *this, history_entry, entry_to_replace, history_handling, user_involvement,
                GC::create_function(heap(), [signal](HistoryStepResult) {
                    signal->resolve({});
                }));

            // FIXME: 2. Invoke WebDriver BiDi fragment navigated with navigable and a new WebDriver BiDi
            //            navigation status whose id is navigationId, url is url, and status is "complete".
            (void)navigation_id;
        }));
    }
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#evaluate-a-javascript:-url
GC::Ptr<DOM::Document> Navigable::evaluate_javascript_url(URL::URL const& url, URL::Origin const& new_document_origin, UserNavigationInvolvement user_involvement, String navigation_id)
{
    auto& vm = this->vm();
    VERIFY(active_window());
    auto& realm = active_window()->realm();

    // 1. Let urlString be the result of running the URL serializer on url.
    auto url_string = url.serialize();

    // 2. Let encodedScriptSource be the result of removing the leading "javascript:" from urlString.
    auto encoded_script_source = url_string.bytes_as_string_view().substring_view(11);

    // 3. Let scriptSource be the UTF-8 decoding of the percent-decoding of encodedScriptSource.
    auto script_source = URL::percent_decode(encoded_script_source);

    // 4. Let settings be targetNavigable's active document's relevant settings object.
    auto& settings = active_document()->relevant_settings_object();

    // 5. Let baseURL be settings's API base URL.
    auto base_url = settings.api_base_url();

    // 6. Let script be the result of creating a classic script given scriptSource, settings, baseURL, and the default classic script fetch options.
    auto script = HTML::ClassicScript::create("(javascript url)", script_source, settings, base_url);

    // 7. Let evaluationStatus be the result of running the classic script script.
    auto evaluation_status = script->run();

    // 8. Let result be null.
    String result;

    // 9. If evaluationStatus is a normal completion, and evaluationStatus.[[Value]] is a String, then set result to evaluationStatus.[[Value]].
    if (evaluation_status.type() == JS::Completion::Type::Normal && evaluation_status.value().is_string()) {
        result = evaluation_status.value().as_string().utf8_string();
    } else {
        // 10. Otherwise, return null.
        return nullptr;
    }

    // 11. Let response be a new response with
    //     URL: targetNavigable's active document's URL
    //     header list: «(`Content-Type`, `text/html;charset=utf-8`)»
    //     body: the UTF-8 encoding of result, as a body
    auto response = Fetch::Infrastructure::Response::create(vm);
    response->url_list().append(active_document()->url());
    response->header_list()->append({ "Content-Type"sv, "text/html"sv });
    response->set_body(Fetch::Infrastructure::byte_sequence_as_body(realm, result.bytes()));

    // 12. Let policyContainer be targetNavigable's active document's policy container.
    auto const& policy_container = active_document()->policy_container();

    // 13. Let finalSandboxFlags be policyContainer's CSP list's CSP-derived sandboxing flags.
    auto final_sandbox_flags = policy_container->csp_list->csp_derived_sandboxing_flags();

    // 14. Let coop be targetNavigable's active document's opener policy.
    auto const& coop = active_document()->opener_policy();

    // 15. Let coopEnforcementResult be a new opener policy enforcement result with
    //     url: url
    //     origin: newDocumentOrigin
    //     opener policy: coop
    OpenerPolicyEnforcementResult coop_enforcement_result {
        .url = url,
        .origin = new_document_origin,
        .opener_policy = coop,
    };

    // AD-HOC: Get the target snapshot params. This is missing from the spec, see https://github.com/whatwg/html/issues/12563
    auto target_snapshot_params = snapshot_target_snapshot_params();

    // 16. Let navigationParams be a new navigation params, with
    //     id: navigationId
    //     navigable: targetNavigable
    //     request: null
    //     response: response
    //     fetch controller: null
    //     commit early hints: null
    //     COOP enforcement result: coopEnforcementResult
    //     reserved environment: null
    //     origin: newDocumentOrigin
    //     policy container: policyContainer
    //     final sandboxing flag set: finalSandboxFlags
    //     iframe element referrer policy: targetSnapshotParams's iframe element referrer policy
    //     opener policy: coop
    //     FIXME: navigation timing type: "navigate"
    //     about base URL: targetNavigable's active document's about base URL
    //     user involvement: userInvolvement
    auto navigation_params = vm.heap().allocate<NavigationParams>(
        navigation_id,
        this,
        nullptr,
        response,
        nullptr,
        nullptr,
        move(coop_enforcement_result),
        nullptr,
        new_document_origin,
        policy_container,
        final_sandbox_flags,
        target_snapshot_params.iframe_element_referrer_policy,
        coop,
        active_document()->about_base_url(),
        user_involvement);

    // 17. Return the result of loading an HTML document given navigationParams.
    // NB: The response body is a known byte sequence, so we can pass it directly for sniffing.
    return load_document(navigation_params, result.bytes());
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#navigate-to-a-javascript:-url
void Navigable::navigate_to_a_javascript_url(URL::URL const& url, HistoryHandlingBehavior history_handling, GC::Ref<SourceSnapshotParams> source_snapshot_params, URL::Origin const& initiator_origin, UserNavigationInvolvement user_involvement, ContentSecurityPolicy::Directives::Directive::NavigationType csp_navigation_type, InitialInsertion initial_insertion, String navigation_id)
{
    auto& vm = this->vm();

    // 1. Assert: historyHandling is "replace".
    VERIFY(history_handling == HistoryHandlingBehavior::Replace);

    // 2. If targetNavigable's ongoing navigation is no longer navigationId, then return.
    // AD-HOC: See https://github.com/whatwg/html/issues/12120, other browsers only cancel pending navigations for form submissions.
    // if (ongoing_navigation() != navigation_id)
    //     return;

    // 3. Set the ongoing navigation for targetNavigable to null.
    set_ongoing_navigation({});

    // 4. If initiatorOrigin is not same origin-domain with targetNavigable's active document's origin, then return.
    if (!initiator_origin.is_same_origin_domain(active_document()->origin()))
        return;

    // 5. Let request be a new request whose URL is url and whose policy container is sourceSnapshotParams's source policy container.
    auto request = Fetch::Infrastructure::Request::create(vm);
    request->set_url(url);
    request->set_policy_container(source_snapshot_params->source_policy_container);

    // AD-HOC: See https://github.com/whatwg/html/issues/4651, requires some investigation to figure out what we should be setting here.
    request->set_client(source_snapshot_params->fetch_client);

    // 6. If the result of should navigation request of type be blocked by Content Security Policy? given request and cspNavigationType is "Blocked", then return.
    if (ContentSecurityPolicy::should_navigation_request_of_type_be_blocked_by_content_security_policy(request, csp_navigation_type) == ContentSecurityPolicy::Directives::Directive::Result::Blocked)
        return;

    // 7. Let newDocument be the result of evaluating a javascript: URL given targetNavigable, url, initiatorOrigin, and userInvolvement.
    auto new_document = evaluate_javascript_url(url, initiator_origin, user_involvement, navigation_id);

    // 8. If newDocument is null:
    if (!new_document) {
        // 1. If initialInsertion is true and targetNavigable's active document's is initial about:blank is true,
        //    then run the iframe load event steps given targetNavigable's container.
        if (initial_insertion == InitialInsertion::Yes && active_document()->is_initial_about_blank()) {
            run_iframe_load_event_steps(as<HTMLIFrameElement>(*container()));
        }

        // AD-HOC: Clear the delaying_load_events flag that was set by begin_navigation step 15.
        //         Since no new document was created, no finalize_a_cross_document_navigation will
        //         run to clear it, which would leave the parent's load event delayed indefinitely.
        set_delaying_load_events(false);

        // 2. Return.
        // NOTE: In this case, some JavaScript code was executed, but no new Document was created, so we will not perform a navigation.
        return;
    }

    // 9. Assert: initiatorOrigin is newDocument's origin.
    VERIFY(initiator_origin == new_document->origin());

    // 10. Let entryToReplace be targetNavigable's active session history entry.
    auto entry_to_replace = active_session_history_entry();

    // 11. Let oldDocState be entryToReplace's document state.
    auto old_doc_state = entry_to_replace->document_state();

    // 12. Let documentState be a new document state with
    //     document: newDocument
    //     history policy container: a clone of the oldDocState's history policy container if it is non-null; null otherwise
    //     request referrer: oldDocState's request referrer
    //     request referrer policy: oldDocState's request referrer policy
    //     initiator origin: initiatorOrigin
    //     origin: initiatorOrigin
    //     about base URL: oldDocState's about base URL
    //     resource: null
    //     ever populated: true
    //     navigable target name: oldDocState's navigable target name
    auto document_state = DocumentState::create();
    document_state->set_history_policy_container(old_doc_state->history_policy_container());
    document_state->set_request_referrer(old_doc_state->request_referrer());
    document_state->set_request_referrer_policy(old_doc_state->request_referrer_policy());
    document_state->set_initiator_origin(initiator_origin);
    document_state->set_origin(initiator_origin);
    document_state->set_about_base_url(old_doc_state->about_base_url());
    document_state->set_ever_populated(true);
    document_state->set_navigable_target_name(old_doc_state->navigable_target_name());
    document_state->set_document_id(new_document->unique_id());

    // 13. Let historyEntry be a new session history entry, with
    //     URL: entryToReplace's URL
    //     document state: documentState
    auto history_entry = SessionHistoryEntry::create();
    history_entry->set_url(entry_to_replace->url());
    history_entry->set_document_state(document_state);

    // 14. Append session history traversal steps to targetNavigable's traversable to finalize a cross-document navigation with targetNavigable, historyHandling, userInvolvement, and historyEntry.
    traversable_navigable()->append_session_history_traversal_steps(GC::create_function(heap(), [this, new_document, history_entry, history_handling, user_involvement](NonnullRefPtr<Core::Promise<Empty>> signal) {
        finalize_a_cross_document_navigation(*this, history_handling, user_involvement, history_entry, new_document, {}, GC::create_function(heap(), [signal](HistoryStepResult) {
            signal->resolve({});
        }));
    }));
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#reload
void Navigable::reload(Optional<SerializationRecord> navigation_api_state, UserNavigationInvolvement user_involvement)
{
    // 1. If userInvolvement is not "browser UI", then:
    if (user_involvement != UserNavigationInvolvement::BrowserUI) {
        // 1. Let navigation be navigable's active window's navigation API.
        auto active_window = this->active_window();
        VERIFY(active_window);
        auto navigation = active_window->navigation();

        // 2. Let destinationNavigationAPIState be navigable's active session history entry's navigation API state.
        auto destination_navigation_api_state = active_session_history_entry()->navigation_api_state();

        // 3. If navigationAPIState is not null, then set destinationNavigationAPIState to navigationAPIState.
        if (navigation_api_state.has_value())
            destination_navigation_api_state = *navigation_api_state;

        // 4. Let continue be the result of firing a push/replace/reload navigate event at navigation with
        //    navigationType set to "reload", isSameDocument set to false, userInvolvement set to userInvolvement,
        //    destinationURL set to navigable's active session history entry's URL, navigationAPIState set to
        //    destinationNavigationAPIState, and apiMethodTracker set to apiMethodTracker.
        auto continue_ = navigation->fire_a_push_replace_reload_navigate_event(Bindings::NavigationType::Reload, active_session_history_entry()->url(), false, user_involvement, nullptr, {}, destination_navigation_api_state);

        // 5. If continue is false, then return.
        if (!continue_)
            return;
    }

    // 1. If navigationAPIState is not null, then set navigable's active session history entry's navigation API state
    //    to navigationAPIState.
    if (navigation_api_state.has_value())
        active_session_history_entry()->set_navigation_api_state(navigation_api_state.release_value());

    // 2. Set navigable's active session history entry's document state's reload pending to true.
    active_session_history_entry()->document_state()->set_reload_pending(true);

    // 3. Let traversable be navigable's traversable navigable.
    auto traversable = traversable_navigable();

    // AD-HOC: Report the reload-pending document state to the UI process before the reload history step finishes,
    //         so the UI-owned session history mirror remains synchronized during an in-flight reload.
    if (traversable->page().client().should_report_session_history_updates()) {
        auto session_history_snapshot = traversable->create_session_history_snapshot();
        traversable->page().client().page_did_update_session_history(session_history_snapshot.top_level_session_history_entries, session_history_snapshot.used_session_history_steps, session_history_snapshot.current_used_step_index);
    }

    // 4. Append the following session history traversal steps to traversable:
    traversable->append_session_history_traversal_steps(GC::create_function(heap(), [traversable, user_involvement](NonnullRefPtr<Core::Promise<Empty>> signal) {
        // 1. Apply the reload history step to traversable given userInvolvement.
        traversable->apply_the_reload_history_step(user_involvement, GC::create_function(traversable->heap(), [signal](HistoryStepResult) {
            signal->resolve({});
        }));
    }));
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#the-navigation-must-be-a-replace
bool navigation_must_be_a_replace(URL::URL const& url, DOM::Document const& document)
{
    return url.scheme() == "javascript"sv || document.is_initial_about_blank();
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#allowed-to-navigate
bool Navigable::allowed_by_sandboxing_to_navigate(Navigable const& target, SourceSnapshotParams const& source_snapshot_params)
{
    auto& source = *this;

    auto is_ancestor_of = [](Navigable const& a, Navigable const& b) {
        for (auto parent = b.parent(); parent; parent = parent->parent()) {
            if (parent.ptr() == &a)
                return true;
        }
        return false;
    };

    // A navigable source is allowed by sandboxing to navigate a second navigable target,
    // given a source snapshot params sourceSnapshotParams, if the following steps return true:

    // 1. If source is target, then return true.
    if (&source == &target)
        return true;

    // 2. If source is an ancestor of target, then return true.
    if (is_ancestor_of(source, target))
        return true;

    // 3. If target is an ancestor of source, then:
    if (is_ancestor_of(target, source)) {

        // 1. If target is not a top-level traversable, then return true.
        if (!target.is_top_level_traversable())
            return true;

        // 2. If sourceSnapshotParams's has transient activation is true, and sourceSnapshotParams's sandboxing flags's
        //    sandboxed top-level navigation with user activation browsing context flag is set, then return false.
        if (source_snapshot_params.has_transient_activation && has_flag(source_snapshot_params.sandboxing_flags, SandboxingFlagSet::SandboxedTopLevelNavigationWithUserActivation))
            return false;

        // 3. If sourceSnapshotParams's has transient activation is false, and sourceSnapshotParams's sandboxing flags's
        //    sandboxed top-level navigation without user activation browsing context flag is set, then return false.
        if (!source_snapshot_params.has_transient_activation && has_flag(source_snapshot_params.sandboxing_flags, SandboxingFlagSet::SandboxedTopLevelNavigationWithoutUserActivation))
            return false;

        // 4. Return true.
        return true;
    }

    // 4. If target is a top-level traversable:
    if (target.is_top_level_traversable()) {
        // FIXME: 1. If source is the one permitted sandboxed navigator of target, then return true.

        // 2. If sourceSnapshotParams's sandboxing flags's sandboxed navigation browsing context flag is set, then return false.
        if (has_flag(source_snapshot_params.sandboxing_flags, SandboxingFlagSet::SandboxedNavigation))
            return false;

        // 3. Return true.
        return true;
    }

    // 5. If sourceSnapshotParams's sandboxing flags's sandboxed navigation browsing context flag is set, then return false.
    // 6. Return true.
    return !has_flag(source_snapshot_params.sandboxing_flags, SandboxingFlagSet::SandboxedNavigation);
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#snapshotting-target-snapshot-params
TargetSnapshotParams Navigable::snapshot_target_snapshot_params()
{
    // To snapshot target snapshot params given a navigable targetNavigable, return a new target snapshot params with:
    // - sandboxing flags: the result of determining the creation sandboxing flags given targetNavigable's active
    //   browsing context and targetNavigable's container
    // - iframe element referrer policy: the result of determining the iframe element referrer policy given
    //   targetNavigable's container
    return {
        .sandboxing_flags = determine_the_creation_sandboxing_flags(*active_browsing_context(), container()),
        .iframe_element_referrer_policy = determine_iframe_element_referrer_policy(container()),
    };
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#finalize-a-cross-document-navigation
void finalize_a_cross_document_navigation(GC::Ref<Navigable> navigable, HistoryHandlingBehavior history_handling, UserNavigationInvolvement user_involvement, NonnullRefPtr<SessionHistoryEntry> history_entry, GC::Ptr<DOM::Document> pending_document, Optional<String> expected_ongoing_navigation_id, GC::Ref<OnApplyHistoryStepComplete> on_complete)
{
    // NOTE: This is not in the spec but we should not navigate destroyed navigable.
    if (navigable->has_been_destroyed()) {
        on_complete->function()(HistoryStepResult::Applied);
        return;
    }

    // 1. FIXME: Assert: this is running on navigable's traversable navigable's session history traversal queue.

    // 2. Set navigable's is delaying load events to false.
    // AD-HOC: Without this guard, decrementing the navigable's delay counter triggers schedule_load_event_delay_check
    //         on the parent, which can see the about:blank (ready_for_post_load_tasks=true) before the session
    //         history traversal activates the new document. The guard is cleared when the new document becomes ready
    //         for post-load tasks (via set_ready_for_post_load_tasks).
    if (auto container_doc = navigable->container_document(); container_doc && pending_document)
        navigable->set_navigation_load_event_guard(*container_doc);

    navigable->set_delaying_load_events(false);

    // 3. If historyEntry's document is null, then return.
    // NOTE: pending_document corresponds to historyEntry's document — it is the document produced by
    //       populate_session_history_entry_document, threaded here explicitly instead of being stored on the entry.
    if (!pending_document) {
        on_complete->function()(HistoryStepResult::Applied);
        return;
    }

    // 4. If all of the following are true:
    //    - navigable's parent is null;
    //    - historyEntry's document's browsing context is not an auxiliary browsing context whose opener browsing context is non-null; and
    //    - historyEntry's document's origin is not navigable's active document's origin
    //    then set historyEntry's document state's navigable target name to the empty string.
    if (navigable->parent() == nullptr
        && !(pending_document->browsing_context()->is_auxiliary() && pending_document->browsing_context()->opener_browsing_context() != nullptr)
        && pending_document->origin() != navigable->active_document()->origin()) {
        history_entry->document_state()->set_navigable_target_name(String {});
    }

    // 5. Let entryToReplace be navigable's active session history entry if historyHandling is "replace", otherwise null.
    auto entry_to_replace = history_handling == HistoryHandlingBehavior::Replace ? navigable->active_session_history_entry() : nullptr;

    // 6. Let traversable be navigable's traversable navigable.
    auto traversable = navigable->traversable_navigable();

    // 7. Let targetStep be null.
    int target_step;

    // 8. Let targetEntries be the result of getting session history entries for navigable.
    Vector<NonnullRefPtr<SessionHistoryEntry>>* target_entries_pointer = nullptr;
    if (navigable.ptr() == traversable.ptr()) {
        target_entries_pointer = &traversable->session_history_entries();
    } else {
        target_entries_pointer = get_session_history_entries_if_present(*traversable, navigable);
        // https://html.spec.whatwg.org/multipage/browsing-the-web.html#getting-session-history-entries
        // AD-HOC: The spec asserts that targetEntries is not null. A queued child-frame commit can run after the
        //         iframe was removed and its nested history list was pruned from the parent document state. Chromium,
        //         WebKit, and Gecko bind child-frame commits to the live frame, so detached frames have no observable
        //         session history effect. Conversely, if this is still the container's live content navigable,
        //         preserve the requested navigation by recreating the missing nested history.
        if (!target_entries_pointer) {
            target_entries_pointer = recreate_missing_nested_history_for_live_child_navigable(*traversable, *navigable);
            if (!target_entries_pointer) {
                navigable->clear_navigation_load_event_guard();
                on_complete->function()(HistoryStepResult::Applied);
                return;
            }
        }
    }
    auto& target_entries = *target_entries_pointer;

    // 9. If entryToReplace is null, then:
    if (entry_to_replace == nullptr) {
        // 1. Clear the forward session history of traversable.
        traversable->clear_the_forward_session_history();

        // 2. Set targetStep to traversable's current session history step + 1.
        // AD-HOC: Claim the step instead — so a step claimed by an apply-history-step run still in flight can't be
        //         handed out twice. See https://github.com/whatwg/html/issues/12576.
        target_step = traversable->claim_next_session_history_step();

        // 3. Set historyEntry's step to targetStep.
        history_entry->set_step(target_step);

        // 4. Append historyEntry to targetEntries.
        target_entries.append(history_entry);
    } else {
        // 1. Replace entryToReplace with historyEntry in targetEntries.
        auto entry_to_replace_iterator = target_entries.find(*entry_to_replace);
        if (entry_to_replace_iterator == target_entries.end()) {
            if (!navigable->active_document()->is_initial_about_blank()) {
                // AD-HOC: A non-initial document whose entryToReplace is no longer in targetEntries is the same
                //         stale child-frame commit case as above.
                navigable->clear_navigation_load_event_guard();
                on_complete->function()(HistoryStepResult::Applied);
                return;
            }

            // https://html.spec.whatwg.org/multipage/document-sequences.html#creating-a-new-child-navigable
            // https://html.spec.whatwg.org/multipage/browsing-the-web.html#finalize-a-cross-document-navigation
            // AD-HOC: Initial about:blank's first real navigation is a replacement. If a synchronous
            //         same-document history update swapped out the original entry object before the queued
            //         cross-document commit runs, replace the remaining initial child entry instead.
            Optional<size_t> entry_to_replace_index;
            for (size_t i = 0; i < target_entries.size(); ++i) {
                if (target_entries[i]->step() == entry_to_replace->step()) {
                    entry_to_replace_index = i;
                    break;
                }
            }
            if (!entry_to_replace_index.has_value() && target_entries.size() == 1)
                entry_to_replace_index = 0;
            if (!entry_to_replace_index.has_value()) {
                navigable->clear_navigation_load_event_guard();
                on_complete->function()(HistoryStepResult::Applied);
                return;
            }

            auto replacement_entry = target_entries[*entry_to_replace_index];
            target_entries[*entry_to_replace_index] = history_entry;
            history_entry->set_step(replacement_entry->step());
        } else {
            *entry_to_replace_iterator = history_entry;

            // 2. Set historyEntry's step to entryToReplace's step.
            history_entry->set_step(entry_to_replace->step());
        }

        // 3. If historyEntry's document state's origin is same origin with entryToReplace's document state's origin,
        //    then set historyEntry's navigation API key to entryToReplace's navigation API key.
        if (history_entry->document_state()->origin().has_value() && entry_to_replace->document_state()->origin().has_value() && history_entry->document_state()->origin()->is_same_origin(*entry_to_replace->document_state()->origin())) {
            history_entry->set_navigation_api_key(entry_to_replace->navigation_api_key());
        }

        // 4. Set targetStep to traversable's current session history step.
        target_step = traversable->current_session_history_step();
    }

    // 10. Apply the push/replace history step targetStep to traversable given historyHandling and userInvolvement.
    traversable->apply_the_push_or_replace_history_step(target_step, history_handling, user_involvement, TraversableNavigable::SynchronousNavigation::No, pending_document, navigable, move(expected_ongoing_navigation_id),
        GC::create_function(navigable->heap(), [on_complete, navigable](HistoryStepResult result) {
            // AD-HOC: Trigger a relayout in the container document for size negotiation with SVG documents.
            if (auto container = navigable->container())
                container->set_needs_layout_update(DOM::SetNeedsLayoutReason::FinalizeACrossDocumentNavigation);
            on_complete->function()(result);
        }));
}

// https://html.spec.whatwg.org/multipage/browsing-the-web.html#url-and-history-update-steps
void perform_url_and_history_update_steps(DOM::Document& document, URL::URL new_url, Optional<SerializationRecord> serialized_data, HistoryHandlingBehavior history_handling)
{
    // 1. Let navigable be document's node navigable.
    auto navigable = document.navigable();

    // 2. Let activeEntry be navigable's active session history entry.
    auto active_entry = navigable->active_session_history_entry();
    navigable->save_persisted_state_to_active_session_history_entry();

    // 3. Let newEntry be a new session history entry, with
    //      URL: newURL
    //      serialized state: if serializedData is not null, serializedData; otherwise activeEntry's classic history API state
    //      document state: activeEntry's document state
    //      scroll restoration mode: activeEntry's scroll restoration mode
    // FIXME: persisted user state: activeEntry's persisted user state
    auto new_entry = SessionHistoryEntry::create();
    new_entry->set_url(new_url);
    new_entry->set_classic_history_api_state(serialized_data.value_or(active_entry->classic_history_api_state()));
    new_entry->set_document_state(active_entry->document_state());
    new_entry->set_scroll_restoration_mode(active_entry->scroll_restoration_mode());
    new_entry->set_scroll_position_data(active_entry->scroll_position_data());

    // 4. If document's is initial about:blank is true, then set historyHandling to "replace".
    if (document.is_initial_about_blank()) {
        history_handling = HistoryHandlingBehavior::Replace;
    }

    // 5. Let entryToReplace be activeEntry if historyHandling is "replace", otherwise null.
    auto entry_to_replace = history_handling == HistoryHandlingBehavior::Replace ? active_entry : nullptr;

    // 6. If historyHandling is "push", then:
    if (history_handling == HistoryHandlingBehavior::Push) {
        // 1. Increment document's history object's index.
        document.history()->m_index++;

        // 2. Set document's history object's length to its index + 1.
        document.history()->m_length = document.history()->m_index + 1;
    }

    // If serializedData is not null, then restore the history object state given document and newEntry.
    if (serialized_data.has_value())
        document.restore_the_history_object_state(new_entry);

    // 8. Set the URL given document to newURL.
    document.set_url(new_url);

    // 9. Set document's latest entry to newEntry.
    document.set_latest_entry(new_entry);

    // 10. Set navigable's active session history entry to newEntry.
    navigable->set_active_session_history_entry(new_entry);

    // 11. Update the navigation API entries for a same-document navigation given document's relevant global object's navigation API, newEntry, and historyHandling.
    auto& relevant_global_object = as<Window>(HTML::relevant_global_object(document));
    auto navigation_type = history_handling == HistoryHandlingBehavior::Push ? Bindings::NavigationType::Push : Bindings::NavigationType::Replace;
    relevant_global_object.navigation()->update_the_navigation_api_entries_for_a_same_document_navigation(new_entry, navigation_type);

    // 12. Let traversable be navigable's traversable navigable.
    auto traversable = navigable->traversable_navigable();

    // AD-HOC: Browser engines commit same-document navigations synchronously when no traversal state is active. Keep
    //         the spec's queued synchronous-navigation steps as the fallback for reentrant traversal work and child
    //         navigables whose nested history is not ready yet.
    // 13. Append the following session history synchronous navigation steps involving navigable to traversable:
    if (!traversable->try_to_synchronously_commit_same_document_navigation(*navigable, new_entry, entry_to_replace)) {
        traversable->append_session_history_synchronous_navigation_steps(*navigable, GC::create_function(document.realm().heap(), [traversable, navigable, new_entry, entry_to_replace, history_handling](NonnullRefPtr<Core::Promise<Empty>> signal) {
            // 1. Finalize a same-document navigation given traversable, navigable, newEntry, entryToReplace,
            //    historyHandling, and "none".
            finalize_a_same_document_navigation(*traversable, *navigable, new_entry, entry_to_replace, history_handling, UserNavigationInvolvement::None,
                GC::create_function(traversable->heap(), [signal](HistoryStepResult) {
                    signal->resolve({});
                }));
            // 2. FIXME: Invoke WebDriver BiDi history updated with navigable.
        }));
    }
}

void Navigable::scroll_offset_did_change()
{
    // https://w3c.github.io/csswg-drafts/cssom-view-1/#scrolling-events
    // Whenever a viewport gets scrolled (whether in response to user interaction or by an API), the user agent must
    // run these steps:

    // 1. Let doc be the viewport’s associated Document.
    auto doc = active_document();
    VERIFY(doc);

    // FIXME: 2. If doc is a snap container, run the steps to update scrollsnapchanging targets for doc with doc’s eventual
    //    snap target in the block axis as newBlockTarget and doc’s eventual snap target in the inline axis as
    //    newInlineTarget.

    // 3. If (doc, "scroll") is already in doc’s pending scroll events, abort these steps.
    if (doc->pending_scroll_events().contains_slow(DOM::Document::PendingScrollEvent { *doc, EventNames::scroll }))
        return;

    // 4. Append (doc, "scroll") to doc’s pending scroll events.
    doc->pending_scroll_events().append({ *doc, EventNames::scroll });
}

CSSPixelRect Navigable::to_top_level_rect(CSSPixelRect const& a_rect)
{
    auto rect = a_rect;
    rect.set_location(to_top_level_position(a_rect.location()));
    return rect;
}

CSSPixelPoint Navigable::to_top_level_position(CSSPixelPoint a_position)
{
    auto position = a_position;
    for (auto ancestor = this; ancestor; ancestor = ancestor->parent()) {
        if (is<TraversableNavigable>(*ancestor))
            break;
        if (!ancestor->container())
            return {};
        auto paintable = ancestor->container()->paintable();
        if (!paintable)
            return {};

        if (auto const* paintable_box = as_if<Painting::PaintableBox>(*paintable)) {
            auto point = paintable_box->absolute_position();
            point.translate_by(position);
            position = paintable_box->transform_rect_to_viewport({ point, { 0, 0 } }).location();
        } else {
            position.translate_by(paintable->box_type_agnostic_position());
        }
    }
    return position;
}

void Navigable::set_viewport_size(CSSPixelSize size, InvalidateDisplayList invalidate_display_list)
{
    if (m_viewport_size == size && invalidate_display_list == InvalidateDisplayList::No)
        return;

    m_viewport_size = size;

    if (has_compositor_context()) {
        compositor_context().viewport_size_updated(
            page().css_to_device_rect(viewport_rect()).size().to_type<int>(),
            Compositor::WindowResizingInProgress::Yes);
        m_pending_set_browser_zoom_request = false;
    }

    if (auto document = active_document()) {
        if (invalidate_display_list == InvalidateDisplayList::Yes)
            document->invalidate_style(DOM::StyleInvalidationReason::NavigableSetViewportSize);
        else
            document->invalidate_style_for_viewport_change();
        document->set_needs_media_query_evaluation();
        document->set_needs_layout_update(DOM::SetNeedsLayoutReason::NavigableSetViewportSize);
    }

    if (auto document = active_document()) {
        document->set_needs_repaint(Badge<HTML::Navigable> {}, invalidate_display_list);

        document->inform_all_viewport_clients_about_the_current_viewport_rect();

        // Schedule the HTML event loop to ensure that a `resize` event gets fired.
        HTML::main_thread_event_loop().schedule();
    }
}

void Navigable::clamp_viewport_scroll_offset()
{
    auto document = active_document();
    if (!document || !document->layout_is_up_to_date())
        return;
    if (!document->paintable_box())
        return;
    auto scrollable_overflow_rect = document->paintable_box()->scrollable_overflow_rect();
    if (!scrollable_overflow_rect.has_value())
        return;
    auto max_x = scrollable_overflow_rect->width() - m_viewport_size.width();
    auto max_y = scrollable_overflow_rect->height() - m_viewport_size.height();
    CSSPixelPoint clamped = {
        max(CSSPixels(0), min(m_viewport_scroll_offset.x(), max_x)),
        max(CSSPixels(0), min(m_viewport_scroll_offset.y(), max_y)),
    };
    if (clamped != m_viewport_scroll_offset)
        perform_scroll_of_viewport_scrolling_box(clamped);
}

void Navigable::perform_scroll_of_viewport_scrolling_box(CSSPixelPoint new_position)
{
    // NB: This method is ad-hoc, but is currently called where "perform a scroll of a scrolling box" would be,
    //     where the box is the viewport.
    //     https://drafts.csswg.org/cssom-view/#perform-a-scroll
    if (m_viewport_scroll_offset != new_position) {
        m_viewport_scroll_offset = new_position;
        scroll_offset_did_change();

        if (auto document = active_document()) {
            document->set_needs_repaint(Badge<HTML::Navigable> {}, InvalidateDisplayList::No);
            document->set_needs_to_refresh_scroll_state(true);
            document->inform_all_viewport_clients_about_the_current_viewport_rect();
        }
    }

    // Schedule the HTML event loop to ensure that a `resize` event gets fired.
    HTML::main_thread_event_loop().schedule();
}

static CSSPixelPoint async_scroll_offset_to_css_pixels(Gfx::FloatPoint async_scroll_offset, double device_pixels_per_css_pixel)
{
    return {
        CSSPixels { async_scroll_offset.x() / device_pixels_per_css_pixel },
        CSSPixels { async_scroll_offset.y() / device_pixels_per_css_pixel },
    };
}

static Optional<CSS::PseudoElement> pseudo_element_from_async_scroll_node_stable_id(Compositor::AsyncScrollNodeStableID const& stable_id)
{
    if (stable_id.kind != Compositor::AsyncScrollNodeKind::PseudoElement)
        return {};
    if (stable_id.pseudo_element_type >= to_underlying(CSS::PseudoElement::KnownPseudoElementCount))
        return {};
    return static_cast<CSS::PseudoElement>(stable_id.pseudo_element_type);
}

static DOM::Element* element_for_async_scroll_node_stable_id(DOM::Document& document, Compositor::AsyncScrollNodeStableID const& stable_id)
{
    auto* node = DOM::Node::from_unique_id(stable_id.node_id);
    auto* element = as_if<DOM::Element>(node);
    if (!element || &element->document() != &document)
        return nullptr;
    return element;
}

static bool adopt_async_element_scroll_delta(DOM::Document& document, Compositor::AsyncScrollNodeStableID const& stable_id, CSSPixelPoint scroll_delta)
{
    auto* element = element_for_async_scroll_node_stable_id(document, stable_id);
    if (!element)
        return false;

    Optional<CSS::PseudoElement> pseudo_element;
    switch (stable_id.kind) {
    case Compositor::AsyncScrollNodeKind::Viewport:
        return false;
    case Compositor::AsyncScrollNodeKind::Element:
        break;
    case Compositor::AsyncScrollNodeKind::PseudoElement:
        pseudo_element = pseudo_element_from_async_scroll_node_stable_id(stable_id);
        if (!pseudo_element.has_value())
            return false;
        if (!element->get_pseudo_element(*pseudo_element).has_value())
            return false;
        break;
    }

    auto scroll_offset = element->scroll_offset(pseudo_element);
    scroll_offset.translate_by(scroll_delta);
    if (element->scroll_offset(pseudo_element) == scroll_offset)
        return false;

    element->set_scroll_offset(pseudo_element, scroll_offset);

    document.set_needs_to_refresh_scroll_state(true);
    if (!document.pending_scroll_events().contains_slow(DOM::Document::PendingScrollEvent { *element, EventNames::scroll }))
        document.pending_scroll_events().append({ *element, EventNames::scroll });
    element->set_needs_repaint(InvalidateDisplayList::No);
    return true;
}

static void queue_async_scroll_operation_promise_resolution(GC::Ref<WebIDL::Promise> promise)
{
    auto& realm = promise->promise()->shape().realm();
    HTML::queue_a_microtask(nullptr, GC::create_function(realm.heap(), [promise] {
        auto& realm = promise->promise()->shape().realm();
        HTML::TemporaryExecutionContext execution_context {
            realm,
            HTML::TemporaryExecutionContext::CallbacksEnabled::Yes
        };
        WebIDL::resolve_promise(realm, promise);
    }));
}

void Navigable::wait_for_async_scroll_operation(Compositor::AsyncScrollOperationID operation_id, GC::Ref<WebIDL::Promise> promise)
{
    if (has_been_destroyed() || !all_navigables().contains(*this)) {
        queue_async_scroll_operation_promise_resolution(promise);
        return;
    }

    m_pending_async_scroll_operations.append({ operation_id, promise });
}

void Navigable::resolve_async_scroll_operation(Compositor::AsyncScrollOperationID operation_id)
{
    m_pending_async_scroll_operations.remove_first_matching([&](auto const& pending) {
        if (pending.operation_id != operation_id)
            return false;

        queue_async_scroll_operation_promise_resolution(pending.promise);
        return true;
    });
}

void Navigable::resolve_all_pending_async_scroll_operations()
{
    while (!m_pending_async_scroll_operations.is_empty()) {
        auto pending = m_pending_async_scroll_operations.take_last();
        queue_async_scroll_operation_promise_resolution(pending.promise);
    }
}

static bool adopt_async_viewport_scroll_delta(Navigable& navigable, CSSPixelPoint scroll_delta)
{
    auto document = navigable.active_document();
    if (!document)
        return false;

    auto visual_viewport = document->visual_viewport();
    CSSPixelPoint page_position { CSSPixels(visual_viewport->page_left()), CSSPixels(visual_viewport->page_top()) };
    auto viewport_scroll_offset = navigable.viewport_scroll_offset();
    navigable.scroll_viewport_by_delta(scroll_delta);

    CSSPixelPoint new_page_position { CSSPixels(visual_viewport->page_left()), CSSPixels(visual_viewport->page_top()) };
    return new_page_position != page_position
        || navigable.viewport_scroll_offset() != viewport_scroll_offset;
}

void Navigable::adopt_pending_async_scroll_offsets()
{
    if (!page().async_scrolling_enabled() || !has_compositor_context())
        return;

    // The compositor process may have already presented newer scroll offsets. Adopt the latest ones before running
    // rendering-update observers so they see the same scroll positions as the user.
    auto async_scroll_updates = compositor_context().take_pending_async_scroll_updates();
    if (async_scroll_updates.scroll_offsets.is_empty() && async_scroll_updates.completed_operation_ids.is_empty())
        return;

    auto document = active_document();
    if (!document) {
        for (auto operation_id : async_scroll_updates.completed_operation_ids)
            resolve_async_scroll_operation(operation_id);
        return;
    }

    auto device_pixels_per_css_pixel = page().client().device_pixels_per_css_pixel();
    bool adopted_any_scroll_delta = false;
    for (auto const& async_scroll_offset : async_scroll_updates.scroll_offsets) {
        auto css_scroll_delta = async_scroll_offset_to_css_pixels(async_scroll_offset.unadopted_scroll_delta, device_pixels_per_css_pixel);
        if (async_scroll_offset.stable_node_id.kind == Compositor::AsyncScrollNodeKind::Viewport) {
            if (async_scroll_offset.stable_node_id.node_id != document->unique_id())
                continue;
            if (adopt_async_viewport_scroll_delta(*this, css_scroll_delta)) {
                adopted_any_scroll_delta = true;
                dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Main thread adopting async viewport delta {},{}",
                    async_scroll_offset.unadopted_scroll_delta.x(), async_scroll_offset.unadopted_scroll_delta.y());
            }
            continue;
        }

        if (adopt_async_element_scroll_delta(*document, async_scroll_offset.stable_node_id, css_scroll_delta)) {
            adopted_any_scroll_delta = true;
            dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Main thread adopting async element delta {},{}",
                async_scroll_offset.unadopted_scroll_delta.x(), async_scroll_offset.unadopted_scroll_delta.y());
        }
    }

    if (adopted_any_scroll_delta)
        schedule_hover_update_after_async_scroll();

    for (auto operation_id : async_scroll_updates.completed_operation_ids)
        resolve_async_scroll_operation(operation_id);
}

void Navigable::schedule_hover_update_after_async_scroll()
{
    static constexpr int hover_update_after_async_scroll_delay_ms = 100;

    if (!m_async_scroll_hover_update_timer) {
        m_async_scroll_hover_update_timer = Core::Timer::create_single_shot(hover_update_after_async_scroll_delay_ms, [this] {
            update_hover_after_async_scroll_stops();
        });
        m_async_scroll_hover_update_timer->start();
        return;
    }

    m_async_scroll_hover_update_timer->restart(hover_update_after_async_scroll_delay_ms);
}

void Navigable::update_hover_after_async_scroll_stops()
{
    if (has_been_destroyed())
        return;
    event_handler().update_hover_after_scroll();
}

void Navigable::cancel_hover_update_after_async_scroll()
{
    if (m_async_scroll_hover_update_timer)
        m_async_scroll_hover_update_timer->stop();
}

// https://html.spec.whatwg.org/multipage/webappapis.html#rendering-opportunity
bool Navigable::has_a_rendering_opportunity() const
{
    // A navigable has a rendering opportunity if the user agent is currently able to present
    // the contents of the navigable to the user,
    // accounting for hardware refresh rate constraints and user agent throttling for performance reasons,
    // but considering content presentable even if it's outside the viewport.

    // A navigable's rendering opportunities are determined based on hardware constraints
    // such as display refresh rates and other factors such as page performance
    // or whether its active document's visibility state is "visible".
    // Rendering opportunities typically occur at regular intervals.

    // FIXME: Return `false` here if we're an inactive browser tab.
    return true;
}

// https://html.spec.whatwg.org/multipage/nav-history-apis.html#inform-the-navigation-api-about-child-navigable-destruction
void Navigable::inform_the_navigation_api_about_child_navigable_destruction()
{
    // 1. Inform the navigation API about aborting navigation in navigable.
    inform_the_navigation_api_about_aborting_navigation();

    // FIXME: 2. Let navigation be navigable's active window's navigation API.

    // FIXME: 3. Let traversalAPIMethodTrackers be a clone of navigation's upcoming traverse API method trackers.

    // FIXME: 4. For each apiMethodTracker of traversalAPIMethodTrackers: reject the finished promise for apiMethodTracker with a new "AbortError" DOMException created in navigation's relevant realm.
}

// https://html.spec.whatwg.org/multipage/nav-history-apis.html#inform-the-navigation-api-about-aborting-navigation
void Navigable::inform_the_navigation_api_about_aborting_navigation()
{
    // 1. If this algorithm is running on navigable's active window's relevant agent's event loop, then continue on to the following steps.
    //    Otherwise, queue a global task on the navigation and traversal task source given navigable's active window to run the following steps.
    // NB: WebContent uses a single main-thread event loop, so the active window's relevant agent is always running on
    //     the current event loop and we run the steps inline. Queuing here would defer the abort past the creation of
    //     a new ongoing navigate event by a subsequent fire_a_push_replace_reload_navigate_event, causing the deferred
    //     abort to cancel that newer event.

    // AD-HOC: Not in the spec but subsequent steps will fail if the navigable doesn't have an active window.
    if (!active_window())
        return;

    HTML::TemporaryExecutionContext execution_context { active_window()->realm() };

    // 2. Let navigation be navigable's active window's navigation API.
    auto navigation = active_window()->navigation();

    // 3. If navigation's ongoing navigate event is null, then return.
    if (navigation->ongoing_navigate_event() == nullptr)
        return;

    // 4. Abort the ongoing navigation given navigation.
    navigation->abort_the_ongoing_navigation();
}

bool Navigable::is_focused() const
{
    if (!m_page->client().has_focus())
        return false;
    return &m_page->focused_navigable() == this;
}

static String visible_text_in_range(DOM::Range const& range)
{
    // NOTE: This is an adaption of Range stringification — but we skip over DOM nodes that don't have a corresponding
    //       layout node, and over nodes whose used value of user-select is 'none'. The latter implements
    //       https://drafts.csswg.org/css-ui/#valdef-user-select-none — applied at the clipboard-extraction boundary.
    StringBuilder builder;

    auto is_user_select_none = [](DOM::Node const& node) {
        auto const* layout = node.layout_node();
        return layout && layout->user_select_used_value() == CSS::UserSelect::None;
    };

    if (range.start_container() == range.end_container() && is<DOM::Text>(*range.start_container())) {
        if (!range.start_container()->layout_node() || is_user_select_none(*range.start_container()))
            return String {};
        return static_cast<DOM::Text const&>(*range.start_container()).data().substring_view(range.start_offset(), range.end_offset() - range.start_offset()).to_utf8_but_should_be_ported_to_utf16();
    }

    if (is<DOM::Text>(*range.start_container()) && range.start_container()->layout_node() && !is_user_select_none(*range.start_container()))
        builder.append(static_cast<DOM::Text const&>(*range.start_container()).data().substring_view(range.start_offset()));

    range.for_each_contained([&](GC::Ref<DOM::Node> node) {
        if (is<DOM::Text>(*node) && node->layout_node() && !is_user_select_none(*node))
            builder.append(static_cast<DOM::Text const&>(*node).data());
        return IterationDecision::Continue;
    });

    if (is<DOM::Text>(*range.end_container()) && range.end_container()->layout_node() && !is_user_select_none(*range.end_container()))
        builder.append(static_cast<DOM::Text const&>(*range.end_container()).data().substring_view(0, range.end_offset()));

    return MUST(builder.to_string());
}

String Navigable::selected_text() const
{
    auto document = active_document();
    if (!document)
        return String {};

    document->update_layout(DOM::UpdateLayoutReason::NavigableSelectedText);

    auto const* input_element = as_if<HTML::HTMLInputElement>(document->active_element());
    if (input_element && input_element->type_state() == HTML::HTMLInputElement::TypeAttributeState::Password) {
        // Apparently nobody wants bullet characters. We leave the clipboard alone here like other browsers.
        return String {};
    }
    auto selection = document->get_selection();
    if (auto form_text = selection->try_form_control_selected_text_for_stringifier(); form_text.has_value())
        return form_text->to_utf8();

    auto range = selection->range();
    if (!range)
        return String {};
    return visible_text_in_range(*range);
}

String Navigable::cut_selected_text() const
{
    auto document = active_document();
    if (!document)
        return {};

    auto* target = document->active_input_events_target();
    if (!target)
        return {};

    auto text = selected_text();
    if (text.is_empty())
        return {};

    target->handle_delete(UIEvents::InputTypes::deleteByCut);
    return text;
}

void Navigable::select_all()
{
    auto document = active_document();
    if (!document)
        return;

    auto selection = document->get_selection();
    if (!selection)
        return;

    if (auto target = document->active_input_events_target()) {
        target->select_all();
    } else if (auto* body = document->body()) {
        (void)selection->select_all_children(*body);
    }
}

void Navigable::paste(Utf16String const& text)
{
    auto document = active_document();
    if (!document)
        return;

    m_event_handler.handle_paste(text);
}

void Navigable::set_marked_text_from_input_method(Utf16String const& text)
{
    // Platform input methods call this on each composition update, with the current marked/preedit text. LibWeb owns
    // the marked-text range – so each update replaces the previously-marked text. The UI doesn't track the preedit
    // extent or pass a replacement length. An empty marked string means there's no preedit: so, clear any text marked
    // thus far, and end the composition — rather than starting or keeping a composition that has no marked text.
    if (text.is_empty()) {
        if (m_input_method_composition_node)
            replace_input_method_marked_text(text);
        m_input_method_composition_node = nullptr;
        return;
    }
    replace_input_method_marked_text(text);
}

void Navigable::commit_text_from_input_method(Utf16String const& text)
{
    // The input method has committed text and finished the composition. Replace the marked text with the committed
    // text, then end the composition — so the text becomes ordinary editable content.
    replace_input_method_marked_text(text);
    m_input_method_composition_node = nullptr;
}

void Navigable::unmark_text_from_input_method()
{
    // The input method has finished the composition — leaving the current marked text in place. End the composition
    // without altering the content.
    m_input_method_composition_node = nullptr;
}

void Navigable::replace_input_method_marked_text(Utf16String const& text)
{
    // Insert text from a platform input method into the currently-focused editable, via the same input-events target
    // that keyboard typing uses — so observers see the correct InputEvent.inputType.
    auto document = active_document();
    if (!document || !document->is_fully_active()) {
        m_input_method_composition_node = nullptr;
        return;
    }
    auto* target = document->active_input_events_target();
    if (!target) {
        m_input_method_composition_node = nullptr;
        return;
    }

    // Drop a stale composition start (for example, if the editable content was replaced out from under us).
    if (m_input_method_composition_node && !m_input_method_composition_node->is_connected())
        m_input_method_composition_node = nullptr;

    // The caret is the end of the marked text. Read it while the selection is still collapsed. Forming the marked-text
    // selection below would otherwise make cursor_position() return null for form controls.
    auto caret = document->cursor_position();
    if (!caret)
        return;

    if (m_input_method_composition_node) {
        // A composition is already in progress. Select the existing marked text [composition start, caret] — so that
        // the insertion below replaces it.
        target->set_selection_anchor(*m_input_method_composition_node, m_input_method_composition_offset);
        target->set_selection_focus(caret->node(), caret->offset());
    } else {
        // Begin a new composition at the caret. The marked text spans from here to the caret as it is updated.
        m_input_method_composition_node = caret->node();
        m_input_method_composition_offset = caret->offset();
    }

    target->handle_insert(UIEvents::InputTypes::insertText, text);
}

// https://drafts.csswg.org/css-view-transitions-1/#snapshot-containing-block
CSSPixelRect Navigable::snapshot_containing_block()
{
    // The snapshot containing block is a rectangle that covers all areas of the window that could potentially display
    // page content (and is therefore consistent regardless of root scrollbars or interactive widgets).

    // Within a child navigable, the snapshot containing block is the union of the navigable’s viewport with any scrollbar gutters.
    // FIXME: Actually get the correct rectangle here.
    return viewport_rect();
}
// https://drafts.csswg.org/css-view-transitions-1/#snapshot-containing-block-size
CSSPixelSize Navigable::snapshot_containing_block_size()
{
    return this->snapshot_containing_block().size();
}

void Navigable::register_navigation_observer(Badge<NavigationObserver>, NavigationObserver& navigation_observer)
{
    m_navigation_observers.append(navigation_observer);
}

void Navigable::unregister_navigation_observer(Badge<NavigationObserver>, NavigationObserver& navigation_observer)
{
    m_navigation_observers.remove(navigation_observer);
}

// https://html.spec.whatwg.org/multipage/document-lifecycle.html#nav-stop
void Navigable::stop_loading()
{
    // 1. Let document be navigable's active document.
    auto document = active_document();

    // 2. If document's unload counter is 0, and navigable's ongoing navigation is a navigation ID, then set the ongoing navigation for navigable to null.
    if (document->unload_counter() == 0 && ongoing_navigation().has<String>())
        set_ongoing_navigation(Empty {});

    // 3. Abort a document and its descendants given document.
    document->abort_a_document_and_its_descendants();
}

void Navigable::set_has_session_history_entry_and_ready_for_navigation()
{
    m_has_session_history_entry_and_ready_for_navigation = true;
    process_pending_navigations();
}

void Navigable::clear_parent_compositor_context()
{
    if (has_compositor_context())
        compositor_context().set_parent_context({});
}

void Navigable::destroy_compositor_context()
{
    clear_parent_compositor_context();
    m_compositor_context.clear();
}

void Navigable::repaint_after_compositor_process_reconnect()
{
    resolve_all_pending_async_scroll_operations();

    if (has_compositor_context()) {
        if (auto parent = this->parent();
            parent && parent->has_compositor_context()) {
            compositor_context().set_parent_context(parent->compositor_context().id());
        }
        compositor_context().viewport_size_updated(
            page().css_to_device_rect(viewport_rect()).size().to_type<int>(),
            Compositor::WindowResizingInProgress::No);

        m_needs_repaint = true;
        m_needs_to_record_display_list = true;
        m_compositor_display_list_paint_config.clear();
        m_compositor_display_list_resources = {};
    }

    for (auto const& child_navigable : child_navigables())
        child_navigable->repaint_after_compositor_process_reconnect();
}

void Navigable::set_should_show_line_box_borders(bool value)
{
    m_should_show_line_box_borders = value;
    set_needs_repaint();

    for (auto const& child_navigable : child_navigables())
        child_navigable->set_should_show_line_box_borders(value);
}

void Navigable::set_should_show_caret_hit_test_debug_overlay(bool value)
{
    m_should_show_caret_hit_test_debug_overlay = value;

    if (auto document = active_document()) {
        if (value)
            document->set_needs_repaint(Badge<HTML::Navigable> {}, InvalidateDisplayList::Yes);
        else
            document->set_caret_hit_test_debug_rect({});
    }

    for (auto const& child_navigable : child_navigables())
        child_navigable->set_should_show_caret_hit_test_debug_overlay(value);
}

bool Navigable::record_display_list_and_scroll_state(PaintConfig paint_config)
{
    if (!has_compositor_context())
        return false;

    m_needs_repaint = false;
    auto document = active_document();
    if (!document)
        return false;

    adopt_pending_async_scroll_offsets();
    document->update_paint_and_hit_testing_properties_if_needed();

    auto should_record_display_list = m_needs_to_record_display_list
        || !m_compositor_display_list_paint_config.has_value()
        || !(m_compositor_display_list_paint_config.value() == paint_config);

    RefPtr<Painting::DisplayList> display_list;
    Painting::DisplayListResourceSet display_list_resources;
    Painting::DisplayListResourceTransaction resource_transaction;
    Optional<Painting::AccumulatedVisualContextTree> visual_context_tree;
    if (should_record_display_list) {
        display_list = document->record_display_list(paint_config, m_display_list_resource_storage);
        if (!display_list)
            return false;
        auto recorded_document_paintable = document->paintable();
        VERIFY(recorded_document_paintable);
        visual_context_tree = recorded_document_paintable->visual_context_tree();
        display_list_resources = m_display_list_resource_storage.collect_referenced_resources(*display_list);
        display_list_resources.include(m_display_list_resource_storage.cache_referenced_resources());
        resource_transaction = m_display_list_resource_storage.create_transaction(
            m_compositor_display_list_resources,
            display_list_resources);
    }

    auto document_paintable = document->paintable();
    VERIFY(document_paintable);
    auto visual_context_tree_needs_compositor_update = document_paintable->visual_context_tree_needs_compositor_update();
    document_paintable->refresh_scroll_state();

    Painting::ScrollStateSnapshot scroll_state_snapshot { document_paintable->scroll_state_snapshot() };
    if (should_record_display_list) {
        compositor_context().update_display_list(*display_list, visual_context_tree.release_value(), move(resource_transaction), move(scroll_state_snapshot));
        document_paintable->did_update_visual_context_tree_in_compositor();
        m_display_list_resource_storage.retain_only(display_list_resources);
        m_compositor_display_list_resources = move(display_list_resources);
        m_needs_to_record_display_list = false;
        m_compositor_display_list_paint_config = paint_config;
    } else {
        if (visual_context_tree_needs_compositor_update) {
            compositor_context().update_visual_context_tree(document_paintable->visual_context_tree());
            document_paintable->did_update_visual_context_tree_in_compositor();
        }
        compositor_context().update_scroll_state(move(scroll_state_snapshot));
    }
    return true;
}

void Navigable::paint_next_frame()
{
    if (has_been_destroyed())
        return;
    if (!has_compositor_context()) {
        m_needs_repaint = false;
        return;
    }

    auto viewport_rect = page().css_to_device_rect(this->viewport_rect()).to_type<int>();
    PaintConfig paint_config { .paint_overlay = true, .should_show_line_box_borders = m_should_show_line_box_borders, .should_show_caret_hit_test_debug_overlay = m_should_show_caret_hit_test_debug_overlay };
    if (is_top_level_traversable()) {
        paint_config.canvas_fill_rect = Gfx::IntRect { {}, viewport_rect.size() };
    } else {
        // Nested navigables paint transparent bitmaps for their parent compositor context.
        if (!parent() || !parent()->has_compositor_context())
            return;
    }

    auto should_defer_main_thread_present_for_async_scroll = [&] {
        return page().async_scrolling_enabled()
            && compositor_context().should_defer_main_thread_present_for_async_scroll();
    };
    if (should_defer_main_thread_present_for_async_scroll())
        return;

    if (!record_display_list_and_scroll_state(paint_config))
        return;

    viewport_rect = page().css_to_device_rect(this->viewport_rect()).to_type<int>();
    if (should_defer_main_thread_present_for_async_scroll()) {
        dbgln_if(COMPOSITOR_DEBUG, "[Compositor] Main thread deferred present while async scroll is pending");
        return;
    }

    compositor_context().present_frame(viewport_rect);
}

void Navigable::render_screenshot(Gfx::PaintingSurface& painting_surface, PaintConfig paint_config, Function<void()>&& callback)
{
    if (!has_compositor_context()) {
        callback();
        return;
    }

    if (!record_display_list_and_scroll_state(paint_config)) {
        callback();
        return;
    }
    compositor_context().request_screenshot(painting_surface, move(callback));
}

GC::Ref<WebIDL::Promise> Navigable::scroll_viewport_by_delta(CSSPixelPoint delta)
{
    auto vv = active_document()->visual_viewport();
    CSSPixelPoint page_position { CSSPixels(vv->page_left()), CSSPixels(vv->page_top()) };
    return perform_a_scroll_of_the_viewport(page_position + delta);
}

// https://drafts.csswg.org/cssom-view/#viewport-perform-a-scroll
GC::Ref<WebIDL::Promise> Navigable::perform_a_scroll_of_the_viewport(CSSPixelPoint position)
{
    // 1. Let doc be the viewport’s associated Document.
    auto doc = active_document();

    // 2. Let vv be the VisualViewport whose associated document is doc.
    auto vv = doc->visual_viewport();

    CSSPixelRect viewport_rect = { m_viewport_scroll_offset, m_viewport_size };

    // 3. Let maxX be the difference between viewport’s scrolling box’s width and the value of vv’s width attribute.
    auto max_x = viewport_rect.width().to_double() - vv->width();

    // 4. Let maxY be the difference between viewport’s scrolling box’s height and the value of vv’s height attribute.
    auto max_y = viewport_rect.height().to_double() - vv->height();

    // 5. Let dx be the horizontal component of position - the value vv’s pageLeft attribute
    auto dx = position.x().to_double() - vv->page_left();

    // 6. Let dy be the vertical component of position - the value of vv’s pageTop attribute
    auto dy = position.y().to_double() - vv->page_top();

    // 7. Let visual x be the value of vv’s offsetLeft attribute.
    auto visual_x = vv->offset_left();

    // 8. Let visual y be the value of vv’s offsetTop attribute.
    auto visual_y = vv->offset_top();

    // 9. Let visual dx be min(maxX, max(0, visual x + dx)) - visual x.
    auto visual_dx = min(max_x, max(0.0, visual_x + dx)) - visual_x;

    // 10. Let visual dy be min(maxY, max(0, visual y + dy)) - visual y.
    auto visual_dy = min(max_y, max(0.0, visual_y + dy)) - visual_y;

    // 11. Let layout dx be dx - visual dx
    auto layout_dx = dx - visual_dx;

    // 12. Let layout dy be dy - visual dy
    auto layout_dy = dy - visual_dy;

    // 13. Let element be doc’s root element if there is one, null otherwise.

    // 14. Perform a scroll of the viewport’s scrolling box to its current scroll position + (layout dx, layout dy)
    //     with element as the associated element, and behavior as the scroll behavior. Let scrollPromise1 be the
    //     Promise returned from this step.
    TemporaryExecutionContext temporary_execution_context { doc->realm() };

    // NB: Must update layout before accessing paintables.
    doc->update_layout(DOM::UpdateLayoutReason::NavigableViewportScroll);

    // AD-HOC: Skip scrolling unscrollable boxes, unless this scroll can pan the visual viewport.
    if (!doc->paintable_box()->could_be_scrolled_by_wheel_event() && visual_dx == 0.0 && visual_dy == 0.0)
        return WebIDL::create_resolved_promise(doc->realm(), JS::js_undefined());

    auto scrolling_area = doc->paintable_box()->scrollable_overflow_rect()->to_type<float>();
    auto new_viewport_scroll_offset = m_viewport_scroll_offset.to_type<double>() + Gfx::Point(layout_dx, layout_dy);
    // NOTE: Clamp to the scrolling area.
    new_viewport_scroll_offset.set_x(max(0.0, min(new_viewport_scroll_offset.x(), scrolling_area.width() - viewport_size().width().to_double())));
    new_viewport_scroll_offset.set_y(max(0.0, min(new_viewport_scroll_offset.y(), scrolling_area.height() - viewport_size().height().to_double())));

    // AD-HOC: If the scroll position would not change, return early to avoid unnecessary display invalidation
    //         and event loop scheduling (e.g. during momentum scrolling against a boundary).
    if (new_viewport_scroll_offset.to_type<CSSPixels>() == m_viewport_scroll_offset && visual_dx == 0.0 && visual_dy == 0.0)
        return WebIDL::create_resolved_promise(doc->realm(), JS::js_undefined());

    // FIXME: Get a Promise from this.
    perform_scroll_of_viewport_scrolling_box(new_viewport_scroll_offset.to_type<CSSPixels>());

    // 15. Perform a scroll of vv’s scrolling box to its current scroll position + (visual dx, visual dy) with element
    //     as the associated element, and behavior as the scroll behavior. Let scrollPromise2 be the Promise returned
    //     from this step.
    // FIXME: Get a Promise from this.
    vv->scroll_by({ visual_dx, visual_dy });
    if (visual_dx == 0.0 && visual_dy == 0.0)
        doc->set_needs_repaint(Badge<HTML::Navigable> {}, InvalidateDisplayList::No);

    // 16. Let scrollPromise be a new Promise.
    auto scroll_promise = WebIDL::create_promise(doc->realm());

    // 17. Return scrollPromise, and run the remaining steps in parallel.
    // 18. Resolve scrollPromise when both scrollPromise1 and scrollPromise2 have settled.
    // FIXME: Actually wait for scroll to occur. For now, all our scrolls are instant.
    WebIDL::resolve_promise(doc->realm(), scroll_promise);
    return scroll_promise;
}

void Navigable::reset_zoom()
{
    auto document = active_document();
    if (!document)
        return;
    document->visual_viewport()->reset();
}

bool Navigable::has_inclusive_ancestor_with_visibility_hidden() const
{
    if (auto container = this->container()) {
        if (auto container_computed_properties = container->computed_properties()) {
            if (container_computed_properties->visibility() == CSS::Visibility::Hidden)
                return true;
        }
        if (auto ancestor_navigable = container->document().navigable()) {
            if (auto ancestor_container = ancestor_navigable->container())
                return ancestor_navigable->has_inclusive_ancestor_with_visibility_hidden();
        }
    }
    return false;
}

}
