// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "extensions/browser/mime_handler/mime_handler_stream_manager.h"

#include <memory>

#include "base/memory/weak_ptr.h"
#include "base/test/run_until.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/mock_navigation_handle.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_renderer_host.h"
#include "extensions/browser/extension_registry.h"
#include "extensions/browser/extensions_browser_client.h"
#include "extensions/browser/mime_handler/mime_handler_body_cache.h"
#include "extensions/browser/mime_handler/mime_handler_test_helpers.h"
#include "extensions/browser/mime_handler/mock_mime_handler_stream_delegate.h"
#include "extensions/browser/mime_handler/stream_container.h"
#include "extensions/browser/mime_handler/stream_info.h"
#include "extensions/browser/test_extensions_browser_client.h"
#include "extensions/browser/unloaded_extension_reason.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_builder.h"
#include "mojo/public/cpp/system/data_pipe.h"
#include "mojo/public/cpp/system/data_pipe_drainer.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/mojom/loader/transferrable_url_loader.mojom.h"
#include "url/gurl.h"

namespace extensions::mime_handler {

namespace {

using ::testing::_;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::SaveArg;

constexpr char kOriginalUrl1[] = "https://original_url1";
constexpr char kOriginalUrl2[] = "https://original_url2";

}  // namespace

class MimeHandlerStreamManagerTest : public content::RenderViewHostTestHarness {
 protected:
  void SetUp() override {
    content::RenderViewHostTestHarness::SetUp();
    // A previous test's BrowserContext may have been allocated at the same
    // address; without this, `DependencyManager::AssertContextWasntDestroyed()`
    // can fail when keyed services are created below.
    BrowserContextDependencyManager::GetInstance()->MarkBrowserContextLive(
        browser_context());
    // `MimeHandlerStreamManager` resolves the `ExtensionRegistry` for its
    // BrowserContext, which requires an `ExtensionsBrowserClient`.
    extensions_browser_client_ =
        std::make_unique<TestExtensionsBrowserClient>(browser_context());
    ExtensionsBrowserClient::Set(extensions_browser_client_.get());
  }

  void TearDown() override {
    // Remove the manager (a registry observer) before the registry is
    // destroyed with the rest of the keyed services.
    content::RenderViewHostTestHarness::web_contents()->RemoveUserData(
        MimeHandlerStreamManager::UserDataKey());
    BrowserContextDependencyManager::GetInstance()
        ->DestroyBrowserContextServices(browser_context());
    ExtensionsBrowserClient::Set(nullptr);
    extensions_browser_client_.reset();
    content::RenderViewHostTestHarness::TearDown();
  }

  MimeHandlerStreamManager* mime_handler_stream_manager() {
    return MimeHandlerStreamManager::FromWebContents(
        content::RenderViewHostTestHarness::web_contents());
  }

  // Simulate a navigation and commit on `host`. The last committed URL will be
  // `original_url`.
  content::RenderFrameHost* NavigateAndCommit(content::RenderFrameHost* host,
                                              const GURL& original_url) {
    content::RenderFrameHost* new_host =
        content::NavigationSimulator::NavigateAndCommitFromDocument(
            original_url, host);

    // Create `MimeHandlerStreamManager` if it doesn't exist already. If `host`
    // is the primary main frame, then the previous `MimeHandlerStreamManager`
    // may have been deleted as part of the above navigation.
    MimeHandlerStreamManager::Create(
        content::RenderViewHostTestHarness::web_contents());
    return new_host;
  }

  content::RenderFrameHost* CreateChildRenderFrameHost(
      content::RenderFrameHost* parent_host) {
    auto* parent_host_tester = content::RenderFrameHostTester::For(parent_host);
    parent_host_tester->InitializeRenderFrameIfNeeded();
    return parent_host_tester->AppendChild("");
  }

  content::RenderFrameHost* CreateAndNavigateChild(
      content::RenderFrameHost* parent,
      const GURL& url) {
    auto* child = CreateChildRenderFrameHost(parent);
    return NavigateAndCommit(child, url);
  }

  void SetUpSimpleStream(content::RenderFrameHost* embedder_host,
                         int container_number,
                         bool embedded) {
    MimeHandlerStreamManager* manager = mime_handler_stream_manager();
    CHECK(manager);
    manager->AddStreamContainer(
        embedder_host->GetFrameTreeNodeId(), "internal_id",
        GenerateSampleStreamContainer(container_number, embedded),
        std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>());
    manager->ClaimStreamInfoForTesting(embedder_host);
  }

  // Stream container number defaults to 1.
  void SetUpStreamWithExtension(content::RenderFrameHost* embedder_host,
                                content::RenderFrameHost* extension_host,
                                bool embedded) {
    SetUpSimpleStream(embedder_host, /*container_number=*/1, embedded);
    mime_handler_stream_manager()->SetExtensionFrameTreeNodeIdForTesting(
        embedder_host, extension_host->GetFrameTreeNodeId());
  }

  void SetUpStreamWithExtensionAndContent(
      content::RenderFrameHost* embedder_host,
      content::RenderFrameHost* extension_host,
      content::FrameTreeNodeId content_frame_tree_node_id,
      bool embedded) {
    SetUpStreamWithExtension(embedder_host, extension_host, embedded);
    mime_handler_stream_manager()->SetContentFrameTreeNodeIdForTesting(
        embedder_host, content_frame_tree_node_id);
  }

  // Notifies registry observers that an extension with `extension_id` was
  // unloaded with `reason`.
  void TriggerOnExtensionUnloaded(const ExtensionId& extension_id,
                                  UnloadedExtensionReason reason) {
    scoped_refptr<const Extension> extension =
        ExtensionBuilder("handler").SetID(extension_id).Build();
    ExtensionRegistry::Get(browser_context())
        ->TriggerOnUnloaded(extension.get(), reason);
  }

 private:
  std::unique_ptr<TestExtensionsBrowserClient> extensions_browser_client_;
};

// Verify adding and getting a `StreamContainer`.
TEST_F(MimeHandlerStreamManagerTest, AddAndGetStreamContainer) {
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  content::FrameTreeNodeId frame_tree_node_id =
      embedder_host->GetFrameTreeNodeId();

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  manager->AddStreamContainer(
      frame_tree_node_id, "internal_id", GenerateSampleStreamContainer(1),
      std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>());
  EXPECT_TRUE(manager->ContainsUnclaimedStreamInfo(frame_tree_node_id));
  manager->ClaimStreamInfoForTesting(embedder_host);

  base::WeakPtr<StreamContainer> result =
      manager->GetStreamContainer(embedder_host);

  ASSERT_TRUE(result);
  blink::mojom::TransferrableURLLoaderPtr transferrable_loader =
      result->TakeTransferrableURLLoader();
  EXPECT_EQ(result->tab_id(), 1);
  EXPECT_EQ(result->embedded(), true);
  EXPECT_EQ(result->handler_url(), GURL("https://handler_url1"));
  EXPECT_EQ(result->extension_id(), "extension_id1");
  EXPECT_EQ(transferrable_loader->url, GURL("stream://url1"));
  EXPECT_EQ(transferrable_loader->head->mime_type, "application/pdf");
  EXPECT_EQ(result->original_url(), GURL("https://original_url1"));
  EXPECT_TRUE(mime_handler_stream_manager());
}

// Verify adding a `StreamContainer` under the same frame tree node ID replaces
// the original unclaimed `StreamContainer`.
TEST_F(MimeHandlerStreamManagerTest,
       AddStreamContainerSameFrameTreeNodeIdUnclaimed) {
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl2));
  content::FrameTreeNodeId frame_tree_node_id =
      embedder_host->GetFrameTreeNodeId();

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  manager->AddStreamContainer(
      frame_tree_node_id, "internal_id1", GenerateSampleStreamContainer(1),
      std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>());
  manager->AddStreamContainer(
      frame_tree_node_id, "internal_id2", GenerateSampleStreamContainer(2),
      std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>());
  manager->ClaimStreamInfoForTesting(embedder_host);

  base::WeakPtr<StreamContainer> result =
      manager->GetStreamContainer(main_rfh());

  ASSERT_TRUE(result);
  blink::mojom::TransferrableURLLoaderPtr transferrable_loader =
      result->TakeTransferrableURLLoader();
  EXPECT_EQ(result->tab_id(), 2);
  EXPECT_EQ(result->embedded(), true);
  EXPECT_EQ(result->handler_url(), GURL("https://handler_url2"));
  EXPECT_EQ(result->extension_id(), "extension_id2");
  EXPECT_EQ(transferrable_loader->url, GURL("stream://url2"));
  EXPECT_EQ(transferrable_loader->head->mime_type, "application/pdf");
  EXPECT_EQ(result->original_url(), GURL("https://original_url2"));
  EXPECT_TRUE(mime_handler_stream_manager());
}

// Verify getting a `StreamContainer` with a non-matching URL returns nullptr;
TEST_F(MimeHandlerStreamManagerTest, AddAndGetStreamInvalidURL) {
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), GURL("https://nonmatching_url"));

  SetUpSimpleStream(embedder_host, /*container_number=*/1, /*embedded=*/true);
  EXPECT_FALSE(
      mime_handler_stream_manager()->GetStreamContainer(embedder_host));
  EXPECT_TRUE(mime_handler_stream_manager());
}

// Verify adding multiple `StreamContainer`s for different FrameTreeNodes
// at once.
TEST_F(MimeHandlerStreamManagerTest, AddMultipleStreamContainers) {
  auto* embedder_host = NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  auto* child_host = CreateAndNavigateChild(embedder_host, GURL(kOriginalUrl2));

  SetUpSimpleStream(embedder_host, /*container_number=*/1, /*embedded=*/true);
  SetUpSimpleStream(child_host, /*container_number=*/2, /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  base::WeakPtr<StreamContainer> result =
      manager->GetStreamContainer(embedder_host);

  ASSERT_TRUE(result);
  blink::mojom::TransferrableURLLoaderPtr transferrable_loader =
      result->TakeTransferrableURLLoader();
  EXPECT_EQ(result->tab_id(), 1);
  EXPECT_EQ(result->embedded(), true);
  EXPECT_EQ(result->handler_url(), GURL("https://handler_url1"));
  EXPECT_EQ(result->extension_id(), "extension_id1");
  EXPECT_EQ(transferrable_loader->url, GURL("stream://url1"));
  EXPECT_EQ(transferrable_loader->head->mime_type, "application/pdf");
  EXPECT_EQ(result->original_url(), GURL("https://original_url1"));

  result = manager->GetStreamContainer(child_host);

  ASSERT_TRUE(result);
  transferrable_loader = result->TakeTransferrableURLLoader();
  EXPECT_EQ(result->tab_id(), 2);
  EXPECT_EQ(result->embedded(), true);
  EXPECT_EQ(result->handler_url(), GURL("https://handler_url2"));
  EXPECT_EQ(result->extension_id(), "extension_id2");
  EXPECT_EQ(transferrable_loader->url, GURL("stream://url2"));
  EXPECT_EQ(transferrable_loader->head->mime_type, "application/pdf");
  EXPECT_EQ(result->original_url(), GURL("https://original_url2"));
  EXPECT_TRUE(mime_handler_stream_manager());
}

// `MimeHandlerStreamManager::IsExtensionHost()` should correctly identify
// the extension hosts.
TEST_F(MimeHandlerStreamManagerTest, IsExtensionHost) {
  auto* embedder_host = CreateAndNavigateChild(main_rfh(), GURL(kOriginalUrl1));

  // During a load, there's an RFH for the extension frame for the initial
  // about:blank navigation. This RFH will always be replaced by
  // `extension_host`.
  auto* about_blank_host = CreateChildRenderFrameHost(embedder_host);
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);
  auto* other_host = CreateChildRenderFrameHost(embedder_host);

  // In a real navigation, `about_blank_host` and `extension_host` would be
  // different RenderFrameHosts in the same FrameTreeNode (sharing the same
  // FrameTreeNodeId). However, the test infrastructure creates them as
  // separate FrameTreeNodes. To simulate the navigation, the stream's
  // extension frame tree node ID is initialized with `about_blank_host`'s
  // FrameTreeNodeId.
  SetUpStreamWithExtension(embedder_host, about_blank_host,
                           /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));

  // `about_blank_host` should be considered an extension host, even if it isn't
  // navigating to the original URL.
  EXPECT_TRUE(manager->IsExtensionHost(about_blank_host));

  // Now, set the extension frame tree node ID to the actual extension frame
  // tree node ID, which will be the same ID as `about_blank_host` in real
  // situations.
  manager->SetExtensionFrameTreeNodeIdForTesting(
      embedder_host, extension_host->GetFrameTreeNodeId());

  EXPECT_TRUE(manager->IsExtensionHost(extension_host));

  // Unrelated hosts shouldn't be considered extension hosts.
  EXPECT_FALSE(manager->IsExtensionHost(other_host));
}

// `MimeHandlerStreamManager::IsExtensionHostForUrl()` additionally requires
// the destination URL to belong to the registered handler extension, so a
// same-FTN navigation to a different extension -- which `IsExtensionHost()`
// alone accepts because it keys only on `FrameTreeNodeId` -- is rejected.
TEST_F(MimeHandlerStreamManagerTest, IsExtensionHostForUrl) {
  auto* embedder_host = CreateAndNavigateChild(main_rfh(), GURL(kOriginalUrl1));

  auto* extension_host = CreateChildRenderFrameHost(embedder_host);

  SetUpStreamWithExtension(embedder_host, extension_host,
                           /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();

  base::WeakPtr<StreamContainer> stream =
      manager->GetStreamContainer(embedder_host);
  ASSERT_TRUE(stream);
  const ExtensionId& handler_id = stream->extension_id();

  // The structural check alone accepts the extension host.
  ASSERT_TRUE(manager->IsExtensionHost(extension_host));

  // A URL owned by the registered handler extension is accepted.
  EXPECT_TRUE(manager->IsExtensionHostForUrl(
      extension_host, Extension::GetBaseURLFromExtensionId(handler_id)));

  // A same-FTN navigation to a different extension is rejected.
  EXPECT_FALSE(manager->IsExtensionHostForUrl(
      extension_host,
      Extension::GetBaseURLFromExtensionId(handler_id + "other")));

  // A non-extension destination is rejected.
  EXPECT_FALSE(manager->IsExtensionHostForUrl(
      extension_host, GURL("https://example.test/index.html")));
}

// Only the extension host directly under the embedder is a secure-context
// root; a same-origin child nested inside it is not.
TEST_F(MimeHandlerStreamManagerTest, SameOriginChildOfExtensionHostIsNotRoot) {
  auto* embedder_host = CreateAndNavigateChild(main_rfh(), GURL(kOriginalUrl1));

  auto* extension_host = CreateChildRenderFrameHost(embedder_host);
  // A same-origin child frame nested inside the extension host (grandchild of
  // the embedder), e.g. an about:blank or another same-extension URL frame.
  auto* same_origin_child = CreateChildRenderFrameHost(extension_host);

  SetUpStreamWithExtension(embedder_host, extension_host,
                           /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();

  base::WeakPtr<StreamContainer> stream =
      manager->GetStreamContainer(embedder_host);
  ASSERT_TRUE(stream);
  const GURL extension_url =
      Extension::GetBaseURLFromExtensionId(stream->extension_id());

  // Precondition: confirm the child is genuinely nested, so the false results
  // below come from the embedder keying, not a null parent.
  ASSERT_EQ(extension_host, same_origin_child->GetParent());

  // The parent (the extension host under the embedder) is a root.
  EXPECT_TRUE(manager->IsExtensionHostForUrl(extension_host, extension_url));

  // The same-origin child is not -- its parent (the extension host) is not an
  // embedder key.
  EXPECT_FALSE(manager->IsExtensionHost(same_origin_child));
  EXPECT_FALSE(
      manager->IsExtensionHostForUrl(same_origin_child, extension_url));
}

// `MimeHandlerStreamManager::IsContentHost()` should correctly identify the
// content hosts.
TEST_F(MimeHandlerStreamManagerTest, IsContentHost) {
  const GURL pdf_url = GURL(kOriginalUrl1);

  auto* embedder_host = CreateAndNavigateChild(main_rfh(), pdf_url);
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);

  // During a load, there's an RFH for the content frame for the initial
  // stream URL navigation. This RFH will always be replaced by
  // `content_host`.
  auto* stream_url_host = CreateChildRenderFrameHost(extension_host);
  auto* content_host = CreateAndNavigateChild(extension_host, pdf_url);
  auto* other_host = CreateChildRenderFrameHost(extension_host);

  // In a real navigation, `stream_url_host` and `content_host` would be
  // different RenderFrameHosts in the same FrameTreeNode (sharing the same
  // FrameTreeNodeId). However, the test infrastructure creates them as
  // separate FrameTreeNodes. To simulate the navigation, the stream's content
  // frame tree node ID is set to `stream_url_host`'s FrameTreeNodeId.
  SetUpStreamWithExtensionAndContent(embedder_host, extension_host,
                                     stream_url_host->GetFrameTreeNodeId(),
                                     /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));

  // `stream_url_host` should be considered a content host, even if it isn't
  // navigating to the original URL.
  EXPECT_TRUE(manager->IsContentHost(stream_url_host));

  // Now, set the content frame tree node ID to the actual content frame tree
  // node ID, which will be the same as `stream_url_host` in real situations.
  manager->SetContentFrameTreeNodeIdForTesting(
      embedder_host, content_host->GetFrameTreeNodeId());

  EXPECT_TRUE(manager->IsContentHost(content_host));

  // Unrelated hosts shouldn't be considered content hosts.
  EXPECT_FALSE(manager->IsContentHost(other_host));
}

// If multiple `StreamContainer`s exist, then deleting one stream shouldn't
// delete the other stream.
TEST_F(MimeHandlerStreamManagerTest, DeleteWithMultipleStreamContainers) {
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  auto* child_host = CreateAndNavigateChild(embedder_host, GURL(kOriginalUrl2));

  SetUpSimpleStream(embedder_host, /*container_number=*/1, /*embedded=*/true);
  SetUpSimpleStream(child_host, /*container_number=*/2, /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));
  ASSERT_TRUE(manager->GetStreamContainer(child_host));

  // `MimeHandlerStreamManager::RenderFrameDeleted()` should cause the stream
  // associated with `child_host` to be deleted.
  manager->RenderFrameDeleted(child_host);

  EXPECT_TRUE(manager->GetStreamContainer(embedder_host));
  EXPECT_FALSE(manager->GetStreamContainer(child_host));
  EXPECT_TRUE(mime_handler_stream_manager());
}

// Verify that unclaimed stream infos can be deleted.
TEST_F(MimeHandlerStreamManagerTest, DeleteUnclaimedStreamInfo) {
  content::RenderFrameHost* unclaimed_embedder_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  content::FrameTreeNodeId frame_tree_node_id =
      unclaimed_embedder_host->GetFrameTreeNodeId();

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  manager->AddStreamContainer(
      frame_tree_node_id, "internal_id", GenerateSampleStreamContainer(1),
      std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>());
  EXPECT_FALSE(manager->GetStreamContainer(unclaimed_embedder_host));

  manager->DeleteUnclaimedStreamInfo(frame_tree_node_id);

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  EXPECT_FALSE(mime_handler_stream_manager());
}

// If the embedder render frame is deleted, the stream should be deleted.
TEST_F(MimeHandlerStreamManagerTest, RenderFrameDeletedWithClaimedStream) {
  auto* actual_host = CreateAndNavigateChild(main_rfh(), GURL(kOriginalUrl1));

  SetUpSimpleStream(actual_host, /*container_number=*/1, /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager->GetStreamContainer(actual_host));

  // Unrelated hosts should be ignored.
  manager->RenderFrameDeleted(main_rfh());
  ASSERT_EQ(manager, mime_handler_stream_manager());

  // `MimeHandlerStreamManager::RenderFrameDeleted()` should cause the stream
  // associated with `actual_host` to be deleted.
  manager->RenderFrameDeleted(actual_host);

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  EXPECT_FALSE(mime_handler_stream_manager());
}

TEST_F(MimeHandlerStreamManagerTest, RenderFrameDeletedWithUnclaimedStream) {
  auto* actual_host = CreateAndNavigateChild(main_rfh(), GURL(kOriginalUrl1));

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  manager->AddStreamContainer(
      actual_host->GetFrameTreeNodeId(), "internal_id",
      GenerateSampleStreamContainer(1),
      std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>());

  // The stream hasn't been claimed, so the stream container can't be retrieved.
  ASSERT_FALSE(manager->GetStreamContainer(actual_host));

  // Unrelated hosts should be ignored.
  manager->RenderFrameDeleted(main_rfh());
  ASSERT_EQ(manager, mime_handler_stream_manager());

  // `MimeHandlerStreamManager::RenderFrameDeleted()` should cause the stream
  // associated with `actual_host` to be deleted.
  manager->RenderFrameDeleted(actual_host);

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  EXPECT_FALSE(mime_handler_stream_manager());
}

// If the `content::RenderFrameHost` for the stream changes, then the stream
// should be deleted.
TEST_F(MimeHandlerStreamManagerTest, EmbedderRenderFrameHostChanged) {
  content::RenderFrameHost* old_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  auto* new_host = CreateChildRenderFrameHost(old_host);

  SetUpSimpleStream(old_host, /*container_number=*/1, /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager->GetStreamContainer(old_host));

  // If the first parameter to RenderFrameHostChanged() is null, then it means a
  // subframe is being created and should be ignored.
  manager->RenderFrameHostChanged(nullptr, old_host);
  EXPECT_TRUE(manager->GetStreamContainer(old_host));

  // Unrelated hosts should be ignored.
  manager->RenderFrameHostChanged(new_host, new_host);
  EXPECT_TRUE(manager->GetStreamContainer(old_host));

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  manager->RenderFrameHostChanged(old_host, new_host);
  EXPECT_FALSE(mime_handler_stream_manager());
}

// If the extension host changes to a different host, the stream should be
// deleted.
TEST_F(MimeHandlerStreamManagerTest, ExtensionRenderFrameHostChanged) {
  auto* embedder_host = CreateAndNavigateChild(main_rfh(), GURL(kOriginalUrl1));

  // During a load, there's an RFH for the extension frame for the initial
  // about:blank navigation. This RFH will always be replaced by
  // `extension_host` and shouldn't trigger stream deletion. Both hosts
  // represent the same frame tree node in real scenarios.
  auto* about_blank_host =
      CreateAndNavigateChild(embedder_host, GURL("about:blank"));
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);
  auto* new_host = CreateChildRenderFrameHost(embedder_host);

  // In a real navigation, `about_blank_host` and `extension_host` would be
  // different RenderFrameHosts in the same FrameTreeNode (sharing the same
  // FrameTreeNodeId). However, the test infrastructure creates them as
  // separate FrameTreeNodes. To simulate the navigation, the stream's
  // extension frame tree node ID is initialized with `about_blank_host`'s
  // FrameTreeNodeId.
  SetUpStreamWithExtension(embedder_host, about_blank_host,
                           /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));

  // Changing `about_blank_host` to `extension_host` shouldn't delete the
  // stream.
  manager->RenderFrameHostChanged(about_blank_host, extension_host);

  ASSERT_TRUE(mime_handler_stream_manager());
  EXPECT_TRUE(manager->GetStreamContainer(embedder_host));

  // Now, set the extension frame tree node ID to the actual extension frame
  // tree node ID, which will be the same ID as `about_blank_host` in real
  // situations.
  manager->SetExtensionFrameTreeNodeIdForTesting(
      embedder_host, extension_host->GetFrameTreeNodeId());

  // Changing the extension host should delete the stream.
  manager->RenderFrameHostChanged(extension_host, new_host);

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  EXPECT_FALSE(mime_handler_stream_manager());
}

// If the content host changes to a different host, the stream should be
// deleted.
TEST_F(MimeHandlerStreamManagerTest, ContentRenderFrameHostChanged) {
  const GURL pdf_url = GURL(kOriginalUrl1);

  auto* embedder_host = CreateAndNavigateChild(main_rfh(), pdf_url);
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);

  // During a load, there's an RFH for the content frame for the initial
  // stream URL navigation. This RFH will always be replaced by
  // `content_host` and shouldn't trigger stream deletion.
  auto* stream_url_host = CreateChildRenderFrameHost(extension_host);
  auto* content_host = CreateAndNavigateChild(extension_host, pdf_url);
  auto* new_host = CreateChildRenderFrameHost(extension_host);

  // In a real navigation, `stream_url_host` and `content_host` would be
  // different RenderFrameHosts in the same FrameTreeNode (sharing the same
  // FrameTreeNodeId). However, the test infrastructure creates them as
  // separate FrameTreeNodes. To simulate the navigation, the stream's content
  // frame tree node ID is set to `stream_url_host`'s FrameTreeNodeId.
  SetUpStreamWithExtensionAndContent(embedder_host, extension_host,
                                     stream_url_host->GetFrameTreeNodeId(),
                                     /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));

  // Changing `stream_url_host` to `content_host` shouldn't delete the stream.
  manager->RenderFrameHostChanged(stream_url_host, content_host);

  ASSERT_TRUE(mime_handler_stream_manager());
  EXPECT_TRUE(manager->GetStreamContainer(embedder_host));

  // Now, set the content frame tree node ID to the actual content frame tree
  // node ID, which will be the same as `stream_url_host` in real situations.
  manager->SetContentFrameTreeNodeIdForTesting(
      embedder_host, content_host->GetFrameTreeNodeId());

  // Changing the content host should delete the stream.
  manager->RenderFrameHostChanged(content_host, new_host);

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  EXPECT_FALSE(mime_handler_stream_manager());
}

TEST_F(MimeHandlerStreamManagerTest,
       ContentRenderFrameHostChangedAllowsFragment) {
  const GURL pdf_url(kOriginalUrl1);
  const GURL pdf_url_with_fragment = pdf_url.Resolve("#fragment");

  auto* embedder_host = CreateAndNavigateChild(main_rfh(), pdf_url);
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);
  auto* content_host =
      CreateAndNavigateChild(extension_host, pdf_url_with_fragment);
  auto* new_host = CreateChildRenderFrameHost(extension_host);

  SetUpStreamWithExtensionAndContent(embedder_host, extension_host,
                                     content_host->GetFrameTreeNodeId(),
                                     /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));

  // Changing the content host should delete the stream.
  manager->RenderFrameHostChanged(content_host, new_host);

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  EXPECT_FALSE(mime_handler_stream_manager());
}

TEST_F(MimeHandlerStreamManagerTest,
       ContentHostChangedCallsValidateContentFrameHost) {
  auto* embedder_host = NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);
  auto* content_host = CreateChildRenderFrameHost(extension_host);
  auto* new_host = CreateChildRenderFrameHost(extension_host);

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  auto delegate = std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>();
  auto* delegate_ptr = delegate.get();
  manager->AddStreamContainer(embedder_host->GetFrameTreeNodeId(),
                              "internal_id", GenerateSampleStreamContainer(1),
                              std::move(delegate));
  manager->ClaimStreamInfoForTesting(embedder_host);
  manager->SetExtensionFrameTreeNodeIdForTesting(
      embedder_host, extension_host->GetFrameTreeNodeId());
  manager->SetContentFrameTreeNodeIdForTesting(
      embedder_host, content_host->GetFrameTreeNodeId());

  auto* stream_info = manager->GetClaimedStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(stream_info);

  EXPECT_CALL(*delegate_ptr,
              ValidateContentFrameHost(content_host, stream_info));

  // Trigger content host change. The content host has an empty last committed
  // URL, so the stream should NOT be deleted (initial RFH changes are ignored).
  manager->RenderFrameHostChanged(content_host, new_host);

  ASSERT_TRUE(mime_handler_stream_manager());
  EXPECT_TRUE(manager->GetStreamContainer(embedder_host));
}

// If the `content::RenderFrameHost` for the stream is deleted, then the stream
// should be deleted.
TEST_F(MimeHandlerStreamManagerTest, EmbedderFrameDeleted) {
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  content::FrameTreeNodeId frame_tree_node_id =
      embedder_host->GetFrameTreeNodeId();

  SetUpSimpleStream(embedder_host, /*container_number=*/1, /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  manager->FrameDeleted(frame_tree_node_id);
  EXPECT_FALSE(mime_handler_stream_manager());
}

// If the extension frame is deleted, the stream should be deleted.
TEST_F(MimeHandlerStreamManagerTest, ExtensionFrameDeleted) {
  auto* embedder_host = CreateAndNavigateChild(main_rfh(), GURL(kOriginalUrl1));
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);
  content::FrameTreeNodeId frame_tree_node_id =
      extension_host->GetFrameTreeNodeId();

  SetUpStreamWithExtension(embedder_host, extension_host, /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));

  // Deleting the extension host should cause the stream to be deleted.
  manager->FrameDeleted(frame_tree_node_id);

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  EXPECT_FALSE(mime_handler_stream_manager());
}

// If the content frame is deleted, the stream should be deleted.
TEST_F(MimeHandlerStreamManagerTest, ContentFrameDeleted) {
  auto* embedder_host = CreateAndNavigateChild(main_rfh(), GURL(kOriginalUrl1));
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);

  auto* content_host = CreateChildRenderFrameHost(extension_host);
  content::FrameTreeNodeId frame_tree_node_id =
      content_host->GetFrameTreeNodeId();

  SetUpStreamWithExtensionAndContent(embedder_host, extension_host,
                                     content_host->GetFrameTreeNodeId(),
                                     /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));

  // Deleting the content host should cause the stream to be deleted.
  manager->FrameDeleted(frame_tree_node_id);

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  EXPECT_FALSE(mime_handler_stream_manager());
}

// The initial load should claim the stream.
TEST_F(MimeHandlerStreamManagerTest,
       ReadyToCommitNavigationCallsOnStreamClaimed) {
  auto* embedder_host = NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();

  auto delegate = std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>();
  auto* delegate_ptr = delegate.get();
  extensions::StreamInfo* captured_stream_info = nullptr;
  EXPECT_CALL(*delegate_ptr, OnStreamClaimed(embedder_host, _))
      .WillOnce(SaveArg<1>(&captured_stream_info));
  manager->AddStreamContainer(embedder_host->GetFrameTreeNodeId(),
                              "internal_id", GenerateSampleStreamContainer(1),
                              std::move(delegate));

  NiceMock<content::MockNavigationHandle> navigation_handle;
  navigation_handle.set_render_frame_host(embedder_host);
  manager->ReadyToCommitNavigation(&navigation_handle);

  auto* stream_info = manager->GetClaimedStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(stream_info);
  EXPECT_EQ(stream_info, captured_stream_info);
}

TEST_F(MimeHandlerStreamManagerTest,
       OnStreamClaimedNotCalledForUnrelatedNavigation) {
  auto* embedder_host = NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  auto* unrelated_host =
      CreateAndNavigateChild(embedder_host, GURL("https://unrelated"));

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  auto delegate = std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>();
  auto* delegate_ptr = delegate.get();
  EXPECT_CALL(*delegate_ptr, OnStreamClaimed(_, _)).Times(0);
  manager->AddStreamContainer(embedder_host->GetFrameTreeNodeId(),
                              "internal_id", GenerateSampleStreamContainer(1),
                              std::move(delegate));

  NiceMock<content::MockNavigationHandle> navigation_handle;
  navigation_handle.set_render_frame_host(unrelated_host);
  manager->ReadyToCommitNavigation(&navigation_handle);

  EXPECT_TRUE(manager->ContainsUnclaimedStreamInfo(
      embedder_host->GetFrameTreeNodeId()));
  EXPECT_FALSE(manager->GetClaimedStreamInfoForTesting(embedder_host));
}

TEST_F(MimeHandlerStreamManagerTest, ReadyToCommitNavigationClaimAndReplace) {
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  manager->AddStreamContainer(
      embedder_host->GetFrameTreeNodeId(), "internal_id",
      GenerateSampleStreamContainer(1),
      std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>());
  EXPECT_FALSE(manager->GetStreamContainer(embedder_host));

  NiceMock<content::MockNavigationHandle> navigation_handle1;
  navigation_handle1.set_render_frame_host(embedder_host);

  // The initial load should cause the embedder host to claim the stream.
  manager->ReadyToCommitNavigation(&navigation_handle1);
  base::WeakPtr<StreamContainer> original_stream =
      manager->GetStreamContainer(embedder_host);
  EXPECT_TRUE(original_stream);
  EXPECT_TRUE(mime_handler_stream_manager());

  NiceMock<content::MockNavigationHandle> navigation_handle2;
  navigation_handle2.set_render_frame_host(embedder_host);

  // Committing a navigation again shouldn't try to claim a stream again if
  // there isn't a new stream. The stream should remain the same. This can occur
  // if a page contains an embed to a handled URL, and the embed later navigates
  // to another URL.
  manager->ReadyToCommitNavigation(&navigation_handle2);
  base::WeakPtr<StreamContainer> same_stream =
      manager->GetStreamContainer(embedder_host);
  ASSERT_TRUE(original_stream);
  ASSERT_TRUE(same_stream);
  EXPECT_EQ(original_stream.get(), same_stream.get());
  EXPECT_TRUE(mime_handler_stream_manager());

  // Re-add a duplicate stream.
  manager->AddStreamContainer(
      embedder_host->GetFrameTreeNodeId(), "internal_id",
      GenerateSampleStreamContainer(1),
      std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>());

  NiceMock<content::MockNavigationHandle> navigation_handle3;
  navigation_handle3.set_render_frame_host(embedder_host);

  // If a new stream exists for the same frame tree node ID, allow claiming the
  // new stream. This can occur if a full page MIME handler refreshes.
  manager->ReadyToCommitNavigation(&navigation_handle3);
  EXPECT_TRUE(manager->GetStreamContainer(embedder_host));
  EXPECT_FALSE(original_stream);
  EXPECT_TRUE(mime_handler_stream_manager());
}

TEST_F(MimeHandlerStreamManagerTest,
       DidFinishNavigationDelegateExtensionFinished) {
  auto* embedder_host = NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  auto delegate = std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>();
  auto* delegate_ptr = delegate.get();
  manager->AddStreamContainer(embedder_host->GetFrameTreeNodeId(),
                              "internal_id", GenerateSampleStreamContainer(1),
                              std::move(delegate));
  manager->ClaimStreamInfoForTesting(embedder_host);
  manager->SetExtensionFrameTreeNodeIdForTesting(
      embedder_host, extension_host->GetFrameTreeNodeId());

  auto* stream_info = manager->GetClaimedStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(stream_info);

  NiceMock<content::MockNavigationHandle> navigation_handle(
      stream_info->stream()->handler_url(), extension_host);
  navigation_handle.set_has_committed(true);

  EXPECT_CALL(*delegate_ptr,
              OnExtensionFrameFinished(&navigation_handle, stream_info));
  manager->DidFinishNavigation(&navigation_handle);
}

class MimeHandlerStreamManagerPostMessageTest
    : public MimeHandlerStreamManagerTest,
      public ::testing::WithParamInterface<bool> {};

TEST_P(MimeHandlerStreamManagerPostMessageTest,
       OnPostMessageSetUpFiresWhenDelegateOptsIn) {
  const bool should_set_up_post_message = GetParam();

  auto* embedder_host = NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);
  auto* content_host = CreateChildRenderFrameHost(extension_host);

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  auto delegate = std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>();
  auto* delegate_ptr = delegate.get();
  manager->AddStreamContainer(embedder_host->GetFrameTreeNodeId(),
                              "internal_id", GenerateSampleStreamContainer(1),
                              std::move(delegate));
  manager->ClaimStreamInfoForTesting(embedder_host);
  manager->SetContentFrameTreeNodeIdForTesting(
      embedder_host, content_host->GetFrameTreeNodeId());

  auto* stream_info = manager->GetClaimedStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(stream_info);

  NiceMock<content::MockNavigationHandle> navigation_handle(
      stream_info->stream()->original_url(), content_host);

  ON_CALL(navigation_handle, IsPdf).WillByDefault(Return(true));
  ON_CALL(*delegate_ptr, ShouldSetUpPostMessage())
      .WillByDefault(Return(should_set_up_post_message));
  EXPECT_CALL(*delegate_ptr, OnPostMessageSetUp(_))
      .Times(should_set_up_post_message ? 1 : 0);

  manager->DidFinishNavigation(&navigation_handle);
}

INSTANTIATE_TEST_SUITE_P(ShouldSetUpPostMessage,
                         MimeHandlerStreamManagerPostMessageTest,
                         ::testing::Bool());

TEST_F(MimeHandlerStreamManagerTest,
       DidFinishNavigationDelegateExtensionFinishedIgnoresNonMatchingUrl) {
  auto* embedder_host = NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);

  SetUpStreamWithExtension(embedder_host, extension_host, /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();

  NiceMock<content::MockNavigationHandle> navigation_handle(GURL(kOriginalUrl2),
                                                            extension_host);
  navigation_handle.set_has_committed(true);
  manager->DidFinishNavigation(&navigation_handle);

  EXPECT_FALSE(manager->DidExtensionFrameFinishNavigation(embedder_host));
}

// Verify `MimeHandlerStreamManager::PluginCanSave()` defaults to false
// and `MimeHandlerStreamManager::SetPluginCanSave()` updates the value.
TEST_F(MimeHandlerStreamManagerTest, PluginCanSave) {
  auto* embedder_host = NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  auto delegate = std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>();
  bool plugin_can_save = false;
  ON_CALL(*delegate, PluginCanSave).WillByDefault([&]() {
    return plugin_can_save;
  });
  ON_CALL(*delegate, SetPluginCanSave).WillByDefault([&](bool can_save) {
    plugin_can_save = can_save;
  });
  manager->AddStreamContainer(embedder_host->GetFrameTreeNodeId(),
                              "internal_id", GenerateSampleStreamContainer(1),
                              std::move(delegate));
  manager->ClaimStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));

  // `MimeHandlerStreamManager::PluginCanSave()` defaults to false.
  EXPECT_FALSE(manager->PluginCanSave(embedder_host));

  // Set to true.
  manager->SetPluginCanSave(embedder_host, true);
  EXPECT_TRUE(manager->PluginCanSave(embedder_host));

  // Set back to false.
  manager->SetPluginCanSave(embedder_host, false);
  EXPECT_FALSE(manager->PluginCanSave(embedder_host));
}

// Verify `MimeHandlerStreamManager::PluginCanSave()` returns false for an
// unknown embedder host, and `MimeHandlerStreamManager::SetPluginCanSave()` on
// an unknown host is a no-op.
TEST_F(MimeHandlerStreamManagerTest, PluginCanSaveUnknownHost) {
  auto* embedder_host = NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  auto* other_host = CreateAndNavigateChild(embedder_host, GURL(kOriginalUrl2));

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  auto delegate = std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>();
  bool plugin_can_save = false;
  ON_CALL(*delegate, PluginCanSave).WillByDefault([&]() {
    return plugin_can_save;
  });
  ON_CALL(*delegate, SetPluginCanSave).WillByDefault([&](bool can_save) {
    plugin_can_save = can_save;
  });
  manager->AddStreamContainer(embedder_host->GetFrameTreeNodeId(),
                              "internal_id", GenerateSampleStreamContainer(1),
                              std::move(delegate));
  manager->ClaimStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));

  // Unknown host should return false (no claimed stream info).
  EXPECT_FALSE(manager->PluginCanSave(other_host));

  // `MimeHandlerStreamManager::SetPluginCanSave()` on unknown host is a
  // no-op -- no crash, and the real host is unaffected.
  manager->SetPluginCanSave(other_host, true);
  EXPECT_FALSE(manager->PluginCanSave(other_host));
  EXPECT_FALSE(manager->PluginCanSave(embedder_host));
}

// Verify PluginCanSave / SetPluginCanSave work through the delegate.
TEST_F(MimeHandlerStreamManagerTest, PluginCanSaveViaDelegate) {
  auto* embedder_host = NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  auto delegate = std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>();
  bool plugin_can_save = false;
  ON_CALL(*delegate, PluginCanSave).WillByDefault([&]() {
    return plugin_can_save;
  });
  ON_CALL(*delegate, SetPluginCanSave).WillByDefault([&](bool can_save) {
    plugin_can_save = can_save;
  });
  manager->AddStreamContainer(embedder_host->GetFrameTreeNodeId(),
                              "internal_id", GenerateSampleStreamContainer(1),
                              std::move(delegate));
  manager->ClaimStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(manager->GetStreamContainer(embedder_host));

  EXPECT_FALSE(manager->PluginCanSave(embedder_host));

  manager->SetPluginCanSave(embedder_host, true);
  EXPECT_TRUE(manager->PluginCanSave(embedder_host));

  manager->SetPluginCanSave(embedder_host, false);
  EXPECT_FALSE(manager->PluginCanSave(embedder_host));
}

// Verify that AddStreamContainer stores the delegate on the stream info.
TEST_F(MimeHandlerStreamManagerTest, AddStreamContainerWithDelegate) {
  auto* embedder_host = NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  auto delegate = std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>();
  auto* delegate_ptr = delegate.get();
  manager->AddStreamContainer(embedder_host->GetFrameTreeNodeId(),
                              "internal_id", GenerateSampleStreamContainer(1),
                              std::move(delegate));
  manager->ClaimStreamInfoForTesting(embedder_host);

  auto* stream_info = manager->GetClaimedStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(stream_info);
  EXPECT_EQ(stream_info->delegate(), delegate_ptr);
}

// `ReadyToCommitNavigation` dispatches to the delegate's
// `OnExtensionFrameReadyToCommit` hook if and only if the committing
// frame is the claimed stream's extension host. Exercise the gating
// conditions with several navigation commits in one flow.
TEST_F(MimeHandlerStreamManagerTest,
       ExtensionFrameReadyToCommitDispatchesToDelegate) {
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();

  auto delegate = std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>();
  auto* delegate_ptr = delegate.get();
  manager->AddStreamContainer(embedder_host->GetFrameTreeNodeId(),
                              "internal_id", GenerateSampleStreamContainer(1),
                              std::move(delegate));

  // The embedder's own commit (top-level, no parent frame) claims the
  // stream and must not dispatch to the extension-frame hook.
  EXPECT_CALL(*delegate_ptr, OnExtensionFrameReadyToCommit(_, _)).Times(0);
  NiceMock<content::MockNavigationHandle> claim_handle;
  claim_handle.set_render_frame_host(embedder_host);
  manager->ReadyToCommitNavigation(&claim_handle);
  testing::Mock::VerifyAndClearExpectations(delegate_ptr);

  // Create the extension child frame but do not mark its FrameTreeNodeId
  // on the stream yet -- simulates the transient about:blank commit
  // before `NavigateToExtensionUrl()` runs. `DidExtensionStartNavigation()`
  // returns false, so the dispatch must not fire.
  content::RenderFrameHost* extension_host =
      CreateChildRenderFrameHost(embedder_host);
  EXPECT_CALL(*delegate_ptr, OnExtensionFrameReadyToCommit(_, _)).Times(0);
  NiceMock<content::MockNavigationHandle> about_blank_handle;
  about_blank_handle.set_render_frame_host(extension_host);
  about_blank_handle.set_is_in_primary_main_frame(false);
  manager->ReadyToCommitNavigation(&about_blank_handle);
  testing::Mock::VerifyAndClearExpectations(delegate_ptr);

  // Mark the extension child frame's FrameTreeNodeId on the stream,
  // simulating `NavigateToExtensionUrl()`.
  manager->SetExtensionFrameTreeNodeIdForTesting(
      embedder_host, extension_host->GetFrameTreeNodeId());

  // A sibling iframe under the same embedder has a different
  // FrameTreeNodeId, so its commit must not dispatch.
  content::RenderFrameHost* sibling_host =
      CreateChildRenderFrameHost(embedder_host);
  EXPECT_CALL(*delegate_ptr, OnExtensionFrameReadyToCommit(_, _)).Times(0);
  NiceMock<content::MockNavigationHandle> sibling_handle;
  sibling_handle.set_render_frame_host(sibling_host);
  sibling_handle.set_is_in_primary_main_frame(false);
  manager->ReadyToCommitNavigation(&sibling_handle);
  testing::Mock::VerifyAndClearExpectations(delegate_ptr);

  // The extension frame's commit satisfies all gating conditions and
  // must dispatch exactly once.
  EXPECT_CALL(*delegate_ptr, OnExtensionFrameReadyToCommit(_, _));
  NiceMock<content::MockNavigationHandle> extension_handle;
  extension_handle.set_render_frame_host(extension_host);
  extension_handle.set_is_in_primary_main_frame(false);
  manager->ReadyToCommitNavigation(&extension_handle);
}

TEST_F(MimeHandlerStreamManagerTest,
       AbortAndFallbackToNativeHandler_MarksEmbedderFrame) {
  const GURL pdf_url(kOriginalUrl1);

  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), pdf_url);
  const content::FrameTreeNodeId embedder_ftn =
      embedder_host->GetFrameTreeNodeId();
  SetUpSimpleStream(embedder_host, /*container_number=*/1, /*embedded=*/true);

  // Satisfy the CHECKs in `AbortAndFallbackToNativeHandler`: the
  // extension frame must have finished navigating, and the content
  // frame must not yet be bound.
  auto* manager = mime_handler_stream_manager();
  auto* stream_info = manager->GetClaimedStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(stream_info);
  stream_info->SetDidExtensionFinishNavigation();

  EXPECT_FALSE(manager->IsPendingNativeFallback(embedder_ftn, pdf_url));
  manager->AbortAndFallbackToNativeHandler(embedder_host);

  // Peek is non-destructive -- the mark must survive until the navigation
  // completes.
  EXPECT_TRUE(manager->IsPendingNativeFallback(embedder_ftn, pdf_url));
  EXPECT_TRUE(manager->IsPendingNativeFallback(embedder_ftn, pdf_url));

  // A different frame is never marked.
  EXPECT_FALSE(
      manager->IsPendingNativeFallback(content::FrameTreeNodeId(), pdf_url));
}

TEST_F(MimeHandlerStreamManagerTest,
       AbortAndFallbackToNativeHandler_DidFinishNavigationClearsMark) {
  const GURL pdf_url(kOriginalUrl1);

  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), pdf_url);
  const content::FrameTreeNodeId embedder_ftn =
      embedder_host->GetFrameTreeNodeId();
  SetUpSimpleStream(embedder_host, /*container_number=*/1, /*embedded=*/true);
  auto* manager = mime_handler_stream_manager();
  auto* stream_info = manager->GetClaimedStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(stream_info);
  stream_info->SetDidExtensionFinishNavigation();

  manager->AbortAndFallbackToNativeHandler(embedder_host);
  ASSERT_TRUE(manager->IsPendingNativeFallback(embedder_ftn, pdf_url));

  // `DidFinishNavigation` on the embedder FTN clears the mark --
  // committed or errored, the re-fetch is over.
  NiceMock<content::MockNavigationHandle> finish_handle(web_contents());
  finish_handle.set_render_frame_host(embedder_host);
  manager->DidFinishNavigation(&finish_handle);
  EXPECT_FALSE(manager->IsPendingNativeFallback(embedder_ftn, pdf_url));
}

TEST_F(MimeHandlerStreamManagerTest,
       AbortAndFallbackToNativeHandler_NoBodyCache_TakeReturnsInvalid) {
  const GURL pdf_url(kOriginalUrl1);

  // Without a body cache attached, the FTN mark still exists but the
  // captured handle is invalid -- the throttle will fall through to a
  // network refetch.
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), pdf_url);
  const content::FrameTreeNodeId embedder_ftn =
      embedder_host->GetFrameTreeNodeId();
  SetUpSimpleStream(embedder_host, /*container_number=*/1, /*embedded=*/true);
  auto* manager = mime_handler_stream_manager();
  auto* stream_info = manager->GetClaimedStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(stream_info);
  stream_info->SetDidExtensionFinishNavigation();

  manager->AbortAndFallbackToNativeHandler(embedder_host);
  ASSERT_TRUE(manager->IsPendingNativeFallback(embedder_ftn, pdf_url));
  EXPECT_FALSE(
      manager->TakeCachedFallbackBody(embedder_ftn, pdf_url).has_value());
}

TEST_F(MimeHandlerStreamManagerTest,
       AbortAndFallbackToNativeHandler_ReplaysCachedBody) {
  const GURL pdf_url(kOriginalUrl1);

  // Populate a body cache, attach it to the claimed stream, abort.
  // `TakeCachedFallbackBody` must return a valid pipe whose bytes match
  // the original body. A second take must return an invalid handle --
  // the underlying mojo data pipe consumer is single-use.
  mojo::ScopedDataPipeProducerHandle producer;
  mojo::ScopedDataPipeConsumerHandle consumer;
  ASSERT_EQ(MOJO_RESULT_OK, mojo::CreateDataPipe(64u, producer, consumer));
  constexpr char kBody[] = "cached-body-bytes";
  ASSERT_EQ(MOJO_RESULT_OK,
            producer->WriteAllData(base::as_byte_span(std::string(kBody))));
  producer.reset();

  auto cache = MimeHandlerBodyCache::Create(std::move(consumer), nullptr);
  ASSERT_TRUE(cache);
  ASSERT_TRUE(base::test::RunUntil([&] { return cache->is_complete(); }));

  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), pdf_url);
  const content::FrameTreeNodeId embedder_ftn =
      embedder_host->GetFrameTreeNodeId();
  auto stream = GenerateSampleStreamContainer(1);
  stream->SetBodyCache(cache);
  auto* manager = mime_handler_stream_manager();
  manager->AddStreamContainer(
      embedder_ftn, "internal_id", std::move(stream),
      std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>());
  manager->ClaimStreamInfoForTesting(embedder_host);
  auto* stream_info = manager->GetClaimedStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(stream_info);
  stream_info->SetDidExtensionFinishNavigation();

  manager->AbortAndFallbackToNativeHandler(embedder_host);
  ASSERT_TRUE(manager->IsPendingNativeFallback(embedder_ftn, pdf_url));

  std::optional<MimeHandlerStreamManager::CachedFallbackBody> taken =
      manager->TakeCachedFallbackBody(embedder_ftn, pdf_url);
  ASSERT_TRUE(taken.has_value());
  ASSERT_TRUE(taken->pipe.is_valid());
  EXPECT_EQ(std::string_view(kBody).size(), taken->decoded_body_size);
  StringDrainerClient client;
  mojo::DataPipeDrainer drainer(&client, std::move(taken->pipe));
  ASSERT_TRUE(base::test::RunUntil([&] { return client.complete(); }));
  EXPECT_EQ(kBody, client.TakeAccumulated());

  // The mark stays in place until `DidFinishNavigation`/`FrameDeleted`,
  // but the body is single-use.
  EXPECT_TRUE(manager->IsPendingNativeFallback(embedder_ftn, pdf_url));
  EXPECT_FALSE(
      manager->TakeCachedFallbackBody(embedder_ftn, pdf_url).has_value());
}

TEST_F(MimeHandlerStreamManagerTest,
       AbortAndFallbackToNativeHandler_RedirectedResponseUrl) {
  const GURL pdf_url(kOriginalUrl1);
  const GURL other_url(kOriginalUrl2);

  // The fallback re-navigation may be redirected to a different URL. The cached
  // body was buffered for `original_url`, so it must not be replayed under the
  // redirected response URL -- the throttle should fall through to the network
  // body for the new URL instead.
  mojo::ScopedDataPipeProducerHandle producer;
  mojo::ScopedDataPipeConsumerHandle consumer;
  ASSERT_EQ(MOJO_RESULT_OK, mojo::CreateDataPipe(64u, producer, consumer));
  constexpr char kBody[] = "cached-body-bytes";
  ASSERT_EQ(MOJO_RESULT_OK,
            producer->WriteAllData(base::as_byte_span(std::string(kBody))));
  producer.reset();

  auto cache = MimeHandlerBodyCache::Create(std::move(consumer), nullptr);
  ASSERT_TRUE(cache);
  ASSERT_TRUE(base::test::RunUntil([&] { return cache->is_complete(); }));

  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), pdf_url);
  const content::FrameTreeNodeId embedder_ftn =
      embedder_host->GetFrameTreeNodeId();
  auto stream = GenerateSampleStreamContainer(1);
  stream->SetBodyCache(cache);
  auto* manager = mime_handler_stream_manager();
  manager->AddStreamContainer(
      embedder_ftn, "internal_id", std::move(stream),
      std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>());
  manager->ClaimStreamInfoForTesting(embedder_host);
  auto* stream_info = manager->GetClaimedStreamInfoForTesting(embedder_host);
  ASSERT_TRUE(stream_info);
  stream_info->SetDidExtensionFinishNavigation();

  manager->AbortAndFallbackToNativeHandler(embedder_host);

  // The mark only applies to the URL the body was cached for. A response for a
  // different URL (e.g. after a server redirect) is not eligible.
  EXPECT_FALSE(manager->IsPendingNativeFallback(embedder_ftn, other_url));
  EXPECT_FALSE(
      manager->TakeCachedFallbackBody(embedder_ftn, other_url).has_value());

  // A fragment-only difference is still the same resource.
  const GURL original_url_with_fragment = pdf_url.Resolve("#fragment");
  EXPECT_TRUE(manager->IsPendingNativeFallback(embedder_ftn,
                                               original_url_with_fragment));

  // The cached body remains available for the matching URL.
  ASSERT_TRUE(manager->IsPendingNativeFallback(embedder_ftn, pdf_url));
  std::optional<MimeHandlerStreamManager::CachedFallbackBody> taken =
      manager->TakeCachedFallbackBody(embedder_ftn, pdf_url);
  ASSERT_TRUE(taken.has_value());
  ASSERT_TRUE(taken->pipe.is_valid());
  StringDrainerClient client;
  mojo::DataPipeDrainer drainer(&client, std::move(taken->pipe));
  ASSERT_TRUE(base::test::RunUntil([&] { return client.complete(); }));
  EXPECT_EQ(kBody, client.TakeAccumulated());
}

TEST_F(MimeHandlerStreamManagerTest,
       TakeCachedFallbackBody_Unflagged_ReturnsInvalid) {
  MimeHandlerStreamManager::Create(web_contents());
  auto* manager = mime_handler_stream_manager();
  ASSERT_TRUE(manager);

  EXPECT_FALSE(manager
                   ->TakeCachedFallbackBody(content::FrameTreeNodeId(),
                                            GURL(kOriginalUrl1))
                   .has_value());
}

// `MimeHandlerStreamManager::GetTopLevelHandlerExtensionId()` returns nullopt
// when no stream is registered for the WebContents.
TEST_F(MimeHandlerStreamManagerTest,
       GetTopLevelHandlerExtensionIdEmptyWithoutStream) {
  // Create the manager but register no streams.
  MimeHandlerStreamManager::Create(
      content::RenderViewHostTestHarness::web_contents());

  EXPECT_FALSE(mime_handler_stream_manager()
                   ->GetTopLevelHandlerExtensionId()
                   .has_value());
}

// `MimeHandlerStreamManager::GetTopLevelHandlerExtensionId()` returns nullopt
// after the claimed stream has been deleted.
TEST_F(MimeHandlerStreamManagerTest,
       GetTopLevelHandlerExtensionIdEmptyAfterStreamDeleted) {
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));

  SetUpSimpleStream(embedder_host, /*container_number=*/1, /*embedded=*/false);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  EXPECT_THAT(manager->GetTopLevelHandlerExtensionId(),
              testing::Optional(std::string("extension_id1")));

  // Navigating away tears down the claimed stream.
  NavigateAndCommit(main_rfh(), GURL(kOriginalUrl2));

  EXPECT_FALSE(mime_handler_stream_manager()
                   ->GetTopLevelHandlerExtensionId()
                   .has_value());
}

// `MimeHandlerStreamManager::GetTopLevelHandlerExtensionId()` returns nullopt
// when the claimed stream's `original_url()` does not match the primary main
// frame's last committed URL -- the URL-spoof guard.
TEST_F(MimeHandlerStreamManagerTest,
       GetTopLevelHandlerExtensionIdEmptyOnUrlMismatch) {
  // Install a stream whose `original_url()` is `kOriginalUrl1`
  // ("https://original_url1") but commit the main frame to a different
  // URL.
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl2));

  SetUpSimpleStream(embedder_host, /*container_number=*/1, /*embedded=*/false);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();

  EXPECT_FALSE(manager->GetTopLevelHandlerExtensionId().has_value());
}

// Unloading the handler extension erases its claimed stream; the manager
// self-deletes once no streams remain.
TEST_F(MimeHandlerStreamManagerTest, UnloadErasesClaimedStream) {
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));

  SetUpSimpleStream(embedder_host, /*container_number=*/1, /*embedded=*/false);
  ASSERT_TRUE(mime_handler_stream_manager()
                  ->GetTopLevelHandlerExtensionId()
                  .has_value());

  TriggerOnExtensionUnloaded("extension_id1", UnloadedExtensionReason::DISABLE);

  EXPECT_FALSE(mime_handler_stream_manager());
}

// Unloading the handler extension erases its unclaimed stream too. TERMINATE
// (extension crash) is handled like any other unload reason.
TEST_F(MimeHandlerStreamManagerTest, UnloadErasesUnclaimedStream) {
  auto stream_container = GenerateSampleStreamContainer(/*container_number=*/1);
  const ExtensionId extension_id = stream_container->extension_id();
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), stream_container->original_url());
  content::FrameTreeNodeId frame_tree_node_id =
      embedder_host->GetFrameTreeNodeId();

  MimeHandlerStreamManager* manager = mime_handler_stream_manager();
  manager->AddStreamContainer(
      frame_tree_node_id, "internal_id", std::move(stream_container),
      std::make_unique<NiceMock<MockMimeHandlerStreamDelegate>>());
  ASSERT_TRUE(manager->ContainsUnclaimedStreamInfo(frame_tree_node_id));

  TriggerOnExtensionUnloaded(extension_id, UnloadedExtensionReason::TERMINATE);

  EXPECT_FALSE(mime_handler_stream_manager());
}

// With streams from two extensions, unloading one erases only its stream and
// the manager stays alive for the other.
TEST_F(MimeHandlerStreamManagerTest, UnloadErasesOnlyUnloadedExtensionStream) {
  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), GURL(kOriginalUrl1));
  content::RenderFrameHost* child_host = NavigateAndCommit(
      CreateChildRenderFrameHost(embedder_host), GURL(kOriginalUrl2));

  SetUpSimpleStream(embedder_host, /*container_number=*/1, /*embedded=*/false);
  SetUpSimpleStream(child_host, /*container_number=*/2, /*embedded=*/true);

  TriggerOnExtensionUnloaded("extension_id1", UnloadedExtensionReason::DISABLE);

  ASSERT_TRUE(mime_handler_stream_manager());
  EXPECT_FALSE(
      mime_handler_stream_manager()->GetStreamContainer(embedder_host));
  EXPECT_TRUE(mime_handler_stream_manager()->GetStreamContainer(child_host));
}

// MimeHandlerStreamManager host privilege status should be correctly revoked
// during a pushState URL path spoofing same-document navigation.
TEST_F(MimeHandlerStreamManagerTest, HostPrivilegeBypassWithPushState) {
  const GURL pdf_url(kOriginalUrl1);

  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), pdf_url);
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);
  auto* content_host = CreateAndNavigateChild(extension_host, pdf_url);

  SetUpStreamWithExtensionAndContent(embedder_host, extension_host,
                                     content_host->GetFrameTreeNodeId(),
                                     /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();

  content::OverrideLastCommittedOrigin(
      extension_host,
      url::Origin::Create(GURL(
          "chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html")));

  // Initially, both extension and content frames should have host privileges.
  EXPECT_TRUE(manager->IsExtensionHost(extension_host));
  EXPECT_TRUE(manager->IsContentHost(content_host));

  // Simulate pushState URL spoofing on the embedder host.
  auto simulator = content::NavigationSimulator::CreateRendererInitiated(
      GURL("https://original_url1/spoofed"), embedder_host);
  simulator->CommitSameDocument();

  // Spoofed URL path must revoke the privileges.
  EXPECT_FALSE(manager->IsExtensionHost(extension_host));
  EXPECT_FALSE(manager->IsContentHost(content_host));
}

// MimeHandlerStreamManager should continue to identify the extension and
// content frames during same-document fragment/hash navigations on the
// embedder host (e.g., scrolling).
TEST_F(MimeHandlerStreamManagerTest, AllowsFragmentNavigation) {
  const GURL pdf_url(kOriginalUrl1);

  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), pdf_url);
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);
  auto* content_host = CreateAndNavigateChild(extension_host, pdf_url);

  SetUpStreamWithExtensionAndContent(embedder_host, extension_host,
                                     content_host->GetFrameTreeNodeId(),
                                     /*embedded=*/false);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();

  content::OverrideLastCommittedOrigin(
      extension_host,
      url::Origin::Create(GURL(
          "chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/index.html")));

  EXPECT_TRUE(manager->IsExtensionHost(extension_host));
  EXPECT_TRUE(manager->IsContentHost(content_host));

  // Simulate a fragment/hash navigation on the embedder host (e.g. scrolling
  // updates hash).
  auto simulator = content::NavigationSimulator::CreateRendererInitiated(
      GURL("https://original_url1#page=2"), embedder_host);
  simulator->CommitSameDocument();

  EXPECT_TRUE(manager->IsExtensionHost(extension_host));
  EXPECT_TRUE(manager->IsContentHost(content_host));

  EXPECT_TRUE(manager->GetStreamContainer(embedder_host));
  EXPECT_THAT(manager->GetTopLevelHandlerExtensionId(),
              testing::Optional(std::string("extension_id1")));
}

// If the extension host changes to a different host after a pushState URL path
// spoofing same-document navigation on the embedder host, the stream should
// still be deleted.
TEST_F(MimeHandlerStreamManagerTest,
       ExtensionRenderFrameHostChangedAfterPushState) {
  const GURL pdf_url(kOriginalUrl1);

  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), pdf_url);
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);
  auto* new_host = CreateChildRenderFrameHost(embedder_host);

  SetUpStreamWithExtension(embedder_host, extension_host,
                           /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();

  // Simulate pushState URL spoofing on the embedder host.
  auto simulator = content::NavigationSimulator::CreateRendererInitiated(
      GURL("https://original_url1/spoofed"), embedder_host);
  simulator->CommitSameDocument();

  // Changing the extension host should delete the stream, even if the embedder
  // host's URL doesn't match the stream's original URL.
  manager->RenderFrameHostChanged(extension_host, new_host);

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  EXPECT_FALSE(mime_handler_stream_manager());
}

// If the content host changes to a different host after a pushState URL path
// spoofing same-document navigation on the embedder host, the stream should
// still be deleted.
TEST_F(MimeHandlerStreamManagerTest,
       ContentRenderFrameHostChangedAfterPushState) {
  const GURL pdf_url(kOriginalUrl1);

  content::RenderFrameHost* embedder_host =
      NavigateAndCommit(main_rfh(), pdf_url);
  auto* extension_host = CreateChildRenderFrameHost(embedder_host);
  auto* content_host = CreateAndNavigateChild(extension_host, pdf_url);
  auto* new_host = CreateChildRenderFrameHost(extension_host);

  SetUpStreamWithExtensionAndContent(embedder_host, extension_host,
                                     content_host->GetFrameTreeNodeId(),
                                     /*embedded=*/true);
  MimeHandlerStreamManager* manager = mime_handler_stream_manager();

  // Simulate pushState URL spoofing on the embedder host.
  auto simulator = content::NavigationSimulator::CreateRendererInitiated(
      GURL("https://original_url1/spoofed"), embedder_host);
  simulator->CommitSameDocument();

  // Changing the content host should delete the stream, even if the embedder
  // host's URL doesn't match the stream's original URL.
  manager->RenderFrameHostChanged(content_host, new_host);

  // There are no remaining streams, so `MimeHandlerStreamManager` should delete
  // itself.
  EXPECT_FALSE(mime_handler_stream_manager());
}

}  // namespace extensions::mime_handler
