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

#include "chrome/browser/resource_coordinator/tab_manager.h"

#include "base/base_switches.h"
#include "base/command_line.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/simple_test_clock.h"
#include "base/test/simple_test_tick_clock.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/devtools/devtools_window_testing.h"
#include "chrome/browser/media/webrtc/media_capture_devices_dispatcher.h"
#include "chrome/browser/media/webrtc/media_stream_capture_indicator.h"
#include "chrome/browser/resource_coordinator/lifecycle_unit_observer.h"
#include "chrome/browser/resource_coordinator/tab_lifecycle_unit.h"
#include "chrome/browser/resource_coordinator/tab_lifecycle_unit_external.h"
#include "chrome/browser/resource_coordinator/tab_lifecycle_unit_source.h"
#include "chrome/browser/resource_coordinator/tab_load_tracker.h"
#include "chrome/browser/resource_coordinator/time.h"
#include "chrome/browser/resource_coordinator/utils.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/find_bar/find_bar_controller.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_features.h"
#include "chrome/common/url_constants.h"
#include "chrome/test/base/chrome_test_path_utils.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/performance_manager/public/features.h"
#include "content/public/browser/navigation_details.h"
#include "content/public/browser/navigation_entry.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_process_host_creation_observer.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/fenced_frame_test_util.h"
#include "content/public/test/test_navigation_observer.h"
#include "content/public/test/test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "third_party/blink/public/mojom/mediastream/media_stream.mojom.h"
#include "url/gurl.h"

using content::OpenURLParams;

#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \
    BUILDFLAG(IS_CHROMEOS)

namespace resource_coordinator {

namespace {

constexpr base::TimeDelta kShortDelay = base::Seconds(1);

bool IsTabDiscarded(content::WebContents* web_contents) {
  return TabLifecycleUnitExternal::FromWebContents(web_contents)
             ->GetTabState() == ::mojom::LifecycleUnitState::DISCARDED;
}

class ExpectStateTransitionObserver : public LifecycleUnitObserver {
 public:
  ExpectStateTransitionObserver(LifecycleUnit* lifecyle_unit,
                                LifecycleUnitState expected_state)
      : lifecycle_unit_(lifecyle_unit), expected_state_(expected_state) {
    lifecycle_unit_->AddObserver(this);
  }

  ExpectStateTransitionObserver(const ExpectStateTransitionObserver&) = delete;
  ExpectStateTransitionObserver& operator=(
      const ExpectStateTransitionObserver&) = delete;

  ~ExpectStateTransitionObserver() override {
    lifecycle_unit_->RemoveObserver(this);
  }

  void AllowState(LifecycleUnitState allowed_state) {
    allowed_states_.insert(allowed_state);
  }

  void Wait() {
    EXPECT_NE(expected_state_, lifecycle_unit_->GetState());
    run_loop_.Run();
    EXPECT_EQ(expected_state_, lifecycle_unit_->GetState());
  }

 private:
  // LifecycleUnitObserver:
  void OnLifecycleUnitStateChanged(LifecycleUnit* lifecycle_unit,
                                   LifecycleUnitState last_state) override {
    EXPECT_EQ(lifecycle_unit, lifecycle_unit_);
    if (lifecycle_unit_->GetState() == expected_state_) {
      run_loop_.Quit();
    } else {
      LOG(ERROR) << "transition to state "
                 << static_cast<int>(lifecycle_unit_->GetState());
      EXPECT_TRUE(allowed_states_.contains(lifecycle_unit_->GetState()));
    }
  }

  const raw_ptr<LifecycleUnit> lifecycle_unit_;
  const LifecycleUnitState expected_state_;
  std::set<LifecycleUnitState> allowed_states_;
  base::RunLoop run_loop_;
};

class DiscardWaiter : public LifecycleUnitObserver {
 public:
  DiscardWaiter() { GetTabLifecycleUnitSource()->AddLifecycleObserver(this); }

  ~DiscardWaiter() override {
    GetTabLifecycleUnitSource()->RemoveLifecycleObserver(this);
  }

  void Wait() { run_loop_.Run(); }

 private:
  void OnLifecycleUnitStateChanged(LifecycleUnit* lifecycle_unit,
                                   LifecycleUnitState last_state) override {
    if (lifecycle_unit->GetState() == mojom::LifecycleUnitState::DISCARDED) {
      run_loop_.Quit();
    }
  }

  base::RunLoop run_loop_;
};

// Allows tests to wait for a renderer process host to exit.
class WindowedRenderProcessHostExitObserver
    : public content::RenderProcessHostObserver {
 public:
  WindowedRenderProcessHostExitObserver() {
    for (auto it = content::RenderProcessHost::AllHostsIterator();
         !it.IsAtEnd(); it.Advance()) {
      host_observation_.AddObservation(it.GetCurrentValue());
    }
  }

  void Wait() {
    if (!seen_) {
      run_loop_.Run();
    }
    EXPECT_TRUE(seen_);
  }

  // content::RenderProcessHostObserver:
  void RenderProcessExited(
      content::RenderProcessHost* host,
      const content::ChildProcessTerminationInfo& info) override {
    seen_ = true;
    host_observation_.RemoveObservation(host);
  }

 private:
  base::ScopedMultiSourceObservation<content::RenderProcessHost,
                                     content::RenderProcessHostObserver>
      host_observation_{this};

  base::RunLoop run_loop_;
  bool seen_ = false;
};

// Ensures that |browser| has |num_tabs| open tabs.
void EnsureTabsInBrowser(BrowserWindowInterface* browser, int num_tabs) {
  for (int i = 0; i < num_tabs; ++i) {
    ui_test_utils::NavigateToURLWithDisposition(
        browser, GURL(chrome::kChromeUICreditsURL),
        i == 0 ? WindowOpenDisposition::CURRENT_TAB
               : WindowOpenDisposition::NEW_BACKGROUND_TAB,
        ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
  }

  EXPECT_EQ(num_tabs, browser->GetTabStripModel()->count());
}

}  // namespace

class TabManagerTest : public InProcessBrowserTest,
                       public ::testing::WithParamInterface<bool> {
 public:
  TabManagerTest()
      : scoped_set_clocks_for_testing_(&test_clock_, &test_tick_clock_) {
    scoped_feature_list_.InitWithFeatureStates(
        {{features::kWebContentsDiscard, IsRetainedWebContents()},
         {performance_manager::features::kTransientKeepAlivePolicy, false}});
    // Start with a non-null TimeTicks, as there is no discard protection for
    // a tab with a null focused timestamp.
    test_tick_clock_.Advance(kShortDelay);
  }

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    host_resolver()->AddRule("*", "127.0.0.1");

    // To avoid flakes when focus changes, set the active tab strip model
    // explicitly.
    focused_tab_strip_model_override_ =
        GetTabLifecycleUnitSource()->SetFocusedTabStripModelForTesting(tsm());
  }

  void TearDownOnMainThread() override {
    // Clear the fakely-focused model before browsers are destroyed.
    focused_tab_strip_model_override_.RunAndReset();
    InProcessBrowserTest::TearDownOnMainThread();
  }

  void OpenTwoTabs(const GURL& first_url, const GURL& second_url) {
    // Open two tabs. Wait for both of them to load.
    content::TestNavigationObserver load1(tsm()->GetActiveWebContents(), 1);
    OpenURLParams open1(first_url, content::Referrer(),
                        WindowOpenDisposition::CURRENT_TAB,
                        ui::PAGE_TRANSITION_TYPED, false);
    browser()->OpenURL(open1, /*navigation_handle_callback=*/{});
    load1.Wait();

    OpenURLParams open2(second_url, content::Referrer(),
                        WindowOpenDisposition::NEW_BACKGROUND_TAB,
                        ui::PAGE_TRANSITION_TYPED, false);
    auto* tab2 = browser()->OpenURL(open2, /*navigation_handle_callback=*/{});
    content::WaitForLoadStop(tab2);

    ASSERT_EQ(2, tsm()->count());
  }

  // Creates a browser with |num_tabs| tabs.
  BrowserWindowInterface* CreateBrowserWithTabs(int num_tabs) {
    BrowserWindowInterface* const current_browser =
        GetLastActiveBrowserWindowInterfaceWithAnyProfile();
    ui_test_utils::BrowserCreatedObserver browser_created_observer;
    chrome::NewWindow(current_browser);
    ui_test_utils::WaitForBrowserSetLastActive(browser_created_observer.Wait());
    BrowserWindowInterface* new_browser =
        GetLastActiveBrowserWindowInterfaceWithAnyProfile();
    EXPECT_NE(new_browser, current_browser);

    // To avoid flakes when focus changes, set the active tab strip model
    // explicitly. Note: we reset the previous one (if any) first; otherwise,
    // the reset would re-set it back to the previous (overridden) value.
    focused_tab_strip_model_override_.RunAndReset();
    focused_tab_strip_model_override_ =
        GetTabLifecycleUnitSource()->SetFocusedTabStripModelForTesting(
            new_browser->GetTabStripModel());

    EnsureTabsInBrowser(new_browser, num_tabs);
    return new_browser;
  }

  TabManager* tab_manager() { return g_browser_process->GetTabManager(); }
  TabStripModel* tsm() { return browser()->GetTabStripModel(); }

  content::WebContents* GetWebContentsAt(int index) {
    return tsm()->GetWebContentsAt(index);
  }

  LifecycleUnit* GetLifecycleUnitAt(int index) {
    return GetTabLifecycleUnitSource()->GetTabLifecycleUnit(
        GetWebContentsAt(index));
  }

  bool IsRetainedWebContents() const { return GetParam(); }

  content::WebContents* UrgentDiscardTabImmediately() {
    return tab_manager()->DiscardTabImpl(LifecycleUnitDiscardReason::URGENT,
                                         /*ignore_recent_visibility=*/true);
  }

  base::SimpleTestClock test_clock_;
  base::SimpleTestTickClock test_tick_clock_;
  ScopedSetClocksForTesting scoped_set_clocks_for_testing_;
  base::test::ScopedFeatureList scoped_feature_list_;
  base::ScopedClosureRunner focused_tab_strip_model_override_;
};

class TabManagerTestWithTwoTabs : public TabManagerTest {
 public:
  TabManagerTestWithTwoTabs() {
    // Tests using two tabs assume that each tab has a dedicated process.
    feature_list_.InitWithFeatures(
        {features::kDisableProcessReuse},
        {performance_manager::features::kTransientKeepAlivePolicy});
  }

  TabManagerTestWithTwoTabs(const TabManagerTestWithTwoTabs&) = delete;
  TabManagerTestWithTwoTabs& operator=(const TabManagerTestWithTwoTabs&) =
      delete;

  void SetUpOnMainThread() override {
    TabManagerTest::SetUpOnMainThread();

    ASSERT_TRUE(embedded_test_server()->Start());

    // Open 2 tabs with default URLs in a focused tab strip.
    OpenTwoTabs(embedded_test_server()->GetURL("/title2.html"),
                embedded_test_server()->GetURL("/title3.html"));
  }

 private:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_P(TabManagerTest, TabManagerBasics) {
  ASSERT_TRUE(embedded_test_server()->Start());
  const GURL url1 = embedded_test_server()->GetURL("a.com", "/title1.html");
  const GURL url2 = embedded_test_server()->GetURL("a.com", "/title2.html");
  const GURL url3 = embedded_test_server()->GetURL("a.com", "/title3.html");

  // Get three tabs open.

  test_tick_clock_.Advance(kShortDelay);
  NavigateToURLWithDisposition(browser(), url1,
                               WindowOpenDisposition::CURRENT_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  test_tick_clock_.Advance(kShortDelay);
  NavigateToURLWithDisposition(browser(), url1,
                               WindowOpenDisposition::NEW_FOREGROUND_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  test_tick_clock_.Advance(kShortDelay);
  NavigateToURLWithDisposition(browser(), url1,
                               WindowOpenDisposition::NEW_FOREGROUND_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
  EXPECT_EQ(3, tsm()->count());

  // Navigate the current (third) tab to a different URL, so we can test
  // back/forward later.
  NavigateToURLWithDisposition(browser(), url2,
                               WindowOpenDisposition::CURRENT_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  // Navigate the third tab again, such that we have three navigation entries.
  NavigateToURLWithDisposition(browser(), url3,
                               WindowOpenDisposition::CURRENT_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
  EXPECT_EQ(3, tsm()->count());

  // Discard a tab.
  EXPECT_TRUE(UrgentDiscardTabImmediately());
  EXPECT_EQ(3, tsm()->count());

  // The first tab should be killed since it was the oldest and was not
  // selected.
  EXPECT_TRUE(IsTabDiscarded(GetWebContentsAt(0)));
  EXPECT_FALSE(IsTabDiscarded(GetWebContentsAt(1)));

  EXPECT_FALSE(IsTabDiscarded(GetWebContentsAt(2)));

  // Run discard again. Both unselected tabs should now be killed.
  EXPECT_TRUE(UrgentDiscardTabImmediately());
  EXPECT_EQ(3, tsm()->count());
  EXPECT_TRUE(IsTabDiscarded(GetWebContentsAt(0)));
  EXPECT_TRUE(IsTabDiscarded(GetWebContentsAt(1)));
  EXPECT_FALSE(IsTabDiscarded(GetWebContentsAt(2)));

  // Run discard again. It should not kill the last tab, since it is active.
  EXPECT_FALSE(UrgentDiscardTabImmediately());
  EXPECT_TRUE(IsTabDiscarded(GetWebContentsAt(0)));
  EXPECT_TRUE(IsTabDiscarded(GetWebContentsAt(1)));
  EXPECT_FALSE(IsTabDiscarded(GetWebContentsAt(2)));

  // Kill the third tab after making second tab active.
  tsm()->ActivateTabAt(1, TabStripUserGestureDetails(
                              TabStripUserGestureDetails::GestureType::kOther));

  EXPECT_EQ(1, tsm()->active_index());
  EXPECT_FALSE(IsTabDiscarded(GetWebContentsAt(1)));
  UrgentDiscardTabImmediately();
  EXPECT_TRUE(IsTabDiscarded(GetWebContentsAt(2)));

  // Force creation of the FindBarController.
  browser()->GetFeatures().GetFindBarController();

  // Select the first tab.  It should reload.
  chrome::SelectNumberedTab(browser(), 0);
  content::WaitForLoadStop(
      browser()->GetTabStripModel()->GetActiveWebContents());
  // Make sure the FindBarController gets the right WebContents.
  EXPECT_EQ(browser()->GetFeatures().GetFindBarController()->web_contents(),
            tsm()->GetActiveWebContents());
  EXPECT_EQ(0, tsm()->active_index());
  EXPECT_FALSE(IsTabDiscarded(GetWebContentsAt(0)));
  EXPECT_FALSE(IsTabDiscarded(GetWebContentsAt(1)));
  EXPECT_TRUE(IsTabDiscarded(GetWebContentsAt(2)));

  // Select the third tab. It should reload.
  chrome::SelectNumberedTab(browser(), 2);
  content::WaitForLoadStop(
      browser()->GetTabStripModel()->GetActiveWebContents());
  EXPECT_EQ(2, tsm()->active_index());
  EXPECT_FALSE(IsTabDiscarded(GetWebContentsAt(0)));
  EXPECT_FALSE(IsTabDiscarded(GetWebContentsAt(1)));
  EXPECT_FALSE(IsTabDiscarded(GetWebContentsAt(2)));

  // Navigate the third tab back twice.  We used to crash here due to
  // crbug.com/40183760.
  EXPECT_TRUE(chrome::CanGoBack(browser()));
  EXPECT_FALSE(chrome::CanGoForward(browser()));
  chrome::GoBack(browser(), WindowOpenDisposition::CURRENT_TAB);
  content::WaitForLoadStop(
      browser()->GetTabStripModel()->GetActiveWebContents());
  EXPECT_TRUE(chrome::CanGoBack(browser()));
  EXPECT_TRUE(chrome::CanGoForward(browser()));
  chrome::GoBack(browser(), WindowOpenDisposition::CURRENT_TAB);
  content::WaitForLoadStop(
      browser()->GetTabStripModel()->GetActiveWebContents());
  EXPECT_FALSE(chrome::CanGoBack(browser()));
  EXPECT_TRUE(chrome::CanGoForward(browser()));
}

// Verify that a discarded tab is considered unloaded by `TabLoadTracker`.
IN_PROC_BROWSER_TEST_P(TabManagerTest, DiscardedTabIsUnloaded) {
  // Setup a browser with one background and one foreground tab.
  ASSERT_TRUE(embedded_test_server()->Start());
  const GURL kURL = embedded_test_server()->GetURL("a.com", "/title1.html");
  NavigateToURLWithDisposition(browser(), kURL,
                               WindowOpenDisposition::CURRENT_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
  NavigateToURLWithDisposition(browser(), kURL,
                               WindowOpenDisposition::NEW_FOREGROUND_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  // Discard the background tab.
  auto* lifecycle_unit_to_discard = GetLifecycleUnitAt(0);
  auto* web_contents = GetWebContentsAt(0);
  ASSERT_EQ(web_contents->GetVisibility(), content::Visibility::HIDDEN);
  lifecycle_unit_to_discard->Discard(LifecycleUnitDiscardReason::URGENT,
                                     /* resident_set_size_estimate=*/0);

  // Get the WebContents at index 0 again. This is necessary because discarding
  // the tab via LifecycleUnit might replace the original WebContents object at
  // that index with a new, empty WebContents. We need to obtain a reference to
  // this newly created WebContents to correctly verify its unloaded state.
  auto* discarded_contents = GetWebContentsAt(0);
  // Verify that it is considered unloaded by `TabLoadTracker`.
  ASSERT_TRUE(discarded_contents->WasDiscarded());
  EXPECT_EQ(TabLoadTracker::Get()->GetLoadingState(discarded_contents),
            TabLoadTracker::LoadingState::UNLOADED);
}

IN_PROC_BROWSER_TEST_P(TabManagerTest, InvalidOrEmptyURL) {
  // Open two tabs. Wait for the foreground one to load but do not wait for the
  // background one.
  NavigateToURLWithDisposition(browser(), GURL(chrome::kChromeUIAboutURL),
                               WindowOpenDisposition::CURRENT_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  NavigateToURLWithDisposition(browser(), GURL(chrome::kChromeUICreditsURL),
                               WindowOpenDisposition::NEW_BACKGROUND_TAB,
                               ui_test_utils::BROWSER_TEST_NO_WAIT);

  ASSERT_EQ(2, tsm()->count());

  // This shouldn't be able to discard a tab as the background tab has not yet
  // started loading (its URL is not committed).
  EXPECT_FALSE(UrgentDiscardTabImmediately());

  // Wait for the background tab to load which then allows it to be discarded.
  content::WaitForLoadStop(browser()->GetTabStripModel()->GetWebContentsAt(1));
  EXPECT_TRUE(UrgentDiscardTabImmediately());
}

// Makes sure that PDF pages are protected.
IN_PROC_BROWSER_TEST_P(TabManagerTest, ProtectPDFPages) {
  // Start the embedded test server so we can get served the required PDF page.
  ASSERT_TRUE(embedded_test_server()->InitializeAndListen());
  embedded_test_server()->StartAcceptingConnections();

  // Get two tabs open, the first one being a PDF page and the second one being
  // the foreground tab.
  GURL url1 = embedded_test_server()->GetURL("/pdf/test.pdf");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url1));

  GURL url2(chrome::kChromeUIAboutURL);
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), url2, WindowOpenDisposition::NEW_FOREGROUND_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  // No discarding should be possible as the only background tab is displaying a
  // PDF page, hence protected.
  EXPECT_FALSE(UrgentDiscardTabImmediately());
}

// Makes sure that tabs using media devices are protected.
IN_PROC_BROWSER_TEST_P(TabManagerTest, ProtectVideoTabs) {
  // Open 2 tabs, the second one being in the background.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser(), GURL(chrome::kChromeUIAboutURL)));
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), GURL(chrome::kChromeUIAboutURL),
      WindowOpenDisposition::NEW_BACKGROUND_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  auto* tab = GetWebContentsAt(1);

  // Simulate that a video stream is now being captured.
  blink::mojom::StreamDevices devices;
  blink::MediaStreamDevice video_device = blink::MediaStreamDevice(
      blink::mojom::MediaStreamType::DEVICE_VIDEO_CAPTURE, "fake_media_device",
      "fake_media_device");
  devices.video_device = video_device;
  MediaCaptureDevicesDispatcher* dispatcher =
      MediaCaptureDevicesDispatcher::GetInstance();
  dispatcher->SetTestVideoCaptureDevices({video_device});
  std::unique_ptr<content::MediaStreamUI> video_stream_ui =
      dispatcher->GetMediaStreamCaptureIndicator()->RegisterMediaStream(
          tab, devices);
  video_stream_ui->OnStarted(base::RepeatingClosure(),
                             content::MediaStreamUI::SourceCallback(),
                             /*label=*/std::string(), /*screen_capture_ids=*/{},
                             content::MediaStreamUI::StateChangeCallback());

  // Should not be able to discard a tab.
  EXPECT_FALSE(UrgentDiscardTabImmediately());

  // Remove the video stream.
  video_stream_ui.reset();

  // Should be able to discard the background tab now.
  EXPECT_TRUE(UrgentDiscardTabImmediately());
}

// Makes sure that tabs using DevTools are protected from discarding.
// TODO(crbug.com/40913262): Flaky on debug Linux.
#if BUILDFLAG(IS_LINUX) && !defined(NDEBUG)
#define MAYBE_ProtectDevToolsTabsFromDiscarding \
  DISABLED_ProtectDevToolsTabsFromDiscarding
#else
#define MAYBE_ProtectDevToolsTabsFromDiscarding \
  ProtectDevToolsTabsFromDiscarding
#endif
IN_PROC_BROWSER_TEST_P(TabManagerTest,
                       MAYBE_ProtectDevToolsTabsFromDiscarding) {
  // Open the first tab.
  NavigateToURLWithDisposition(browser(), GURL(chrome::kChromeUIAboutURL),
                               WindowOpenDisposition::CURRENT_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  // Open a DevTools window for the first.
  DevToolsWindow* devtool = DevToolsWindowTesting::OpenDevToolsWindowSync(
      GetWebContentsAt(0), true /* is_docked */);
  EXPECT_TRUE(devtool);

  // Open the second tab.
  NavigateToURLWithDisposition(browser(), GURL(chrome::kChromeUICreditsURL),
                               WindowOpenDisposition::NEW_FOREGROUND_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  // No discarding should be possible as the only background tab is currently
  // using DevTools.
  EXPECT_FALSE(UrgentDiscardTabImmediately());

  // Close the DevTools window and repeat the test, this time use a non-docked
  // window.
  DevToolsWindowTesting::CloseDevToolsWindowSync(devtool);
  devtool = DevToolsWindowTesting::OpenDevToolsWindowSync(
      GetWebContentsAt(0), false /* is_docked */);
  EXPECT_TRUE(devtool);
  EXPECT_FALSE(UrgentDiscardTabImmediately());

  // Close the DevTools window, ensure that the tab can be discarded.
  DevToolsWindowTesting::CloseDevToolsWindowSync(devtool);
  EXPECT_TRUE(UrgentDiscardTabImmediately());
}

IN_PROC_BROWSER_TEST_P(TabManagerTest, AutoDiscardable) {
  // Get two tabs open.
  NavigateToURLWithDisposition(browser(), GURL(chrome::kChromeUIAboutURL),
                               WindowOpenDisposition::CURRENT_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  NavigateToURLWithDisposition(browser(), GURL(chrome::kChromeUICreditsURL),
                               WindowOpenDisposition::NEW_FOREGROUND_TAB,
                               ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
  ASSERT_EQ(2, tsm()->count());

  // Set the auto-discardable state of the first tab to false.
  TabLifecycleUnitExternal::FromWebContents(GetWebContentsAt(0))
      ->SetAutoDiscardable(false);

  // Shouldn't discard the tab, since auto-discardable is deactivated.
  EXPECT_FALSE(UrgentDiscardTabImmediately());

  // Reset auto-discardable state to true.
  TabLifecycleUnitExternal::FromWebContents(GetWebContentsAt(0))
      ->SetAutoDiscardable(true);

  // Now it should be able to discard the tab.
  EXPECT_TRUE(UrgentDiscardTabImmediately());
  EXPECT_TRUE(IsTabDiscarded(GetWebContentsAt(0)));
}

IN_PROC_BROWSER_TEST_P(TabManagerTestWithTwoTabs,
                       UrgentFastShutdownSingleTabProcess) {
  // The Tab Manager should be able to fast-kill a process for the discarded tab
  // on all platforms, as each tab will be running in a separate process by
  // itself regardless of the discard reason.
  WindowedRenderProcessHostExitObserver observer;
  EXPECT_TRUE(UrgentDiscardTabImmediately());
  observer.Wait();
}

IN_PROC_BROWSER_TEST_P(TabManagerTest, UrgentFastShutdownSharedTabProcess) {
  ASSERT_TRUE(embedded_test_server()->Start());

  // Set max renderers to 1 before opening tabs to force running out of
  // processes and for both these tabs to share a renderer.
  content::RenderProcessHost::SetMaxRendererProcessCount(1);
  OpenTwoTabs(embedded_test_server()->GetURL("a.com", "/title1.html"),
              embedded_test_server()->GetURL("a.com", "/title2.html"));
  EXPECT_EQ(tsm()->GetWebContentsAt(0)->GetPrimaryMainFrame()->GetProcess(),
            tsm()->GetWebContentsAt(1)->GetPrimaryMainFrame()->GetProcess());

  // The Tab Manager will not be able to fast-kill either of the tabs since they
  // share the same process regardless of the discard reason. An unsafe attempt
  // will be made on some platforms.
  EXPECT_TRUE(UrgentDiscardTabImmediately());
}

IN_PROC_BROWSER_TEST_P(TabManagerTest, UrgentFastShutdownWithUnloadHandler) {
  ASSERT_TRUE(embedded_test_server()->Start());
  // Disable the protection of recent tabs.
  OpenTwoTabs(embedded_test_server()->GetURL("a.com", "/title1.html"),
              embedded_test_server()->GetURL("/unload.html"));

  // The Tab Manager will not be able to safely fast-kill either of the tabs as
  // one of them is current, and the other has an unload handler. An unsafe
  // attempt will be made on some platforms.
#if BUILDFLAG(IS_CHROMEOS)
  // The unsafe attempt for ChromeOS should succeed as ChromeOS ignores unload
  // handlers when in critical condition.
  WindowedRenderProcessHostExitObserver observer;
#endif  // BUILDFLAG(IS_CHROMEOS)
  EXPECT_TRUE(UrgentDiscardTabImmediately());
#if BUILDFLAG(IS_CHROMEOS)
  observer.Wait();
#endif  // BUILDFLAG(IS_CHROMEOS)
}

IN_PROC_BROWSER_TEST_P(TabManagerTest,
                       UrgentFastShutdownWithBeforeunloadHandler) {
  ASSERT_TRUE(embedded_test_server()->Start());
  // Disable the protection of recent tabs.
  OpenTwoTabs(embedded_test_server()->GetURL("a.com", "/title1.html"),
              embedded_test_server()->GetURL("/beforeunload.html"));

  // The Tab Manager will not be able to safely fast-kill either of the tabs as
  // one of them is current, and the other has a beforeunload handler. An unsafe
  // attempt will be made on some platforms.
  EXPECT_TRUE(UrgentDiscardTabImmediately());
}

class TabManagerIgnoreWorkersTest : public TabManagerTest {
 public:
  TabManagerIgnoreWorkersTest() {
    scoped_feature_list_.InitWithFeaturesAndParameters(
        {{features::kWebContentsDiscard,
          {{"urgent_discard_ignore_workers", "true"}}}},
        {});
  }

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

IN_PROC_BROWSER_TEST_P(TabManagerIgnoreWorkersTest,
                       UrgentFastShutdownWithWorker) {
  base::HistogramTester histogram_tester;

  embedded_test_server()->ServeFilesFromSourceDirectory("content/test/data");
  ASSERT_TRUE(embedded_test_server()->Start());
  OpenTwoTabs(embedded_test_server()->GetURL("a.com", "/title1.html"),
              embedded_test_server()->GetURL("/register_service_worker.html"));

  // Wait for the service worker to become ready.
  content::RenderFrameHost* main_frame =
      tsm()->GetWebContentsAt(1)->GetPrimaryMainFrame();
  EXPECT_EQ("DONE",
            EvalJs(main_frame, "register('/fetch_event_passthrough.js')"));

  // The Tab Manager will not be able to safely fast-kill either of the tabs as
  // one of them is current, and the other has a service worker. An unsafe
  // attempt will be made on some platforms.
#if BUILDFLAG(IS_CHROMEOS)
  // The unsafe attempt for ChromeOS should succeed as ChromeOS ignores unload
  // handlers and workers when in critical condition.
  WindowedRenderProcessHostExitObserver observer;
#endif  // BUILDFLAG(IS_CHROMEOS)
  EXPECT_TRUE(UrgentDiscardTabImmediately());
#if BUILDFLAG(IS_CHROMEOS)
  observer.Wait();
  histogram_tester.ExpectBucketCount(
      "Discarding.AttemptFastKillForDiscardResult",
      AttemptFastKillForDiscardResult::kKilledWithoutUnloadHandlersAndWorkers,
      1);
#else
  histogram_tester.ExpectBucketCount(
      "Discarding.AttemptFastKillForDiscardResult",
      AttemptFastKillForDiscardResult::kSkipped, 1);
#endif  // BUILDFLAG(IS_CHROMEOS)
}

// Verifies the following state transitions for a tab:
// - Initial state: ACTIVE
// - Discard(kUrgent): ACTIVE->DISCARDED
// - Navigate: DISCARDED->ACTIVE
//             window.document.wasDiscarded is true
IN_PROC_BROWSER_TEST_P(TabManagerTestWithTwoTabs, TabUrgentDiscardAndNavigate) {
  const char kDiscardedStateJS[] = "window.document.wasDiscarded;";

  GURL test_page(chrome_test_utils::GetTestUrl(
      base::FilePath(), base::FilePath(FILE_PATH_LITERAL("simple.html"))));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page));

  // document.wasDiscarded is false initially.
  EXPECT_EQ(false, content::EvalJs(GetWebContentsAt(0), kDiscardedStateJS));

  // Discard the tab.
  EXPECT_EQ(LifecycleUnitState::ACTIVE, GetLifecycleUnitAt(0)->GetState());
  EXPECT_TRUE(
      GetLifecycleUnitAt(0)->Discard(LifecycleUnitDiscardReason::EXTERNAL));
  EXPECT_EQ(LifecycleUnitState::DISCARDED, GetLifecycleUnitAt(0)->GetState());

  // Here we simulate re-focussing the tab causing reload with navigation,
  // the navigation will reload the tab.
  // TODO(fdoray): Figure out why the test fails if a reload is done instead of
  // a navigation.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page));
  EXPECT_EQ(LifecycleUnitState::ACTIVE, GetLifecycleUnitAt(0)->GetState());

  // document.wasDiscarded is true on navigate after discard.
  EXPECT_EQ(true, content::EvalJs(GetWebContentsAt(0), kDiscardedStateJS));
}

IN_PROC_BROWSER_TEST_P(TabManagerTestWithTwoTabs,
                       EmitsLatencyMetrics_NoFastShutdown) {
  base::HistogramTester histogram_tester;

  const GURL test_page(chrome_test_utils::GetTestUrl(
      base::FilePath(), base::FilePath(FILE_PATH_LITERAL("simple.html"))));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page));

  // Increment the worker ref count of the renderer process to keep it alive and
  // guarantee the renderer is able to perform the discard and notify the
  // Browser.
  content::RenderProcessHost* process =
      GetWebContentsAt(0)->GetPrimaryMainFrame()->GetProcess();
  EXPECT_TRUE(process->IsInitializedAndNotDead());
  process->IncrementWorkerRefCount();

  // Discard the tab.
  EXPECT_EQ(LifecycleUnitState::ACTIVE, GetLifecycleUnitAt(0)->GetState());
  EXPECT_TRUE(
      GetLifecycleUnitAt(0)->Discard(LifecycleUnitDiscardReason::EXTERNAL));
  EXPECT_EQ(LifecycleUnitState::DISCARDED, GetLifecycleUnitAt(0)->GetState());

  // Assert the Browser acknowledges and records the discard latency.
  EXPECT_TRUE(base::test::RunUntil([&] {
    return histogram_tester
               .GetAllSamples("Discarding.TabLifecycleUnit.DiscardLatency")
               .size() == 1;
  }));
  process->DecrementWorkerRefCount();
}

IN_PROC_BROWSER_TEST_P(TabManagerTestWithTwoTabs,
                       EmitsLatencyMetrics_FastShutdown) {
  base::HistogramTester histogram_tester;

  const GURL test_page(chrome_test_utils::GetTestUrl(
      base::FilePath(), base::FilePath(FILE_PATH_LITERAL("simple.html"))));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page));

  // Increment the worker ref count of the renderer process to keep it alive and
  // guarantee the renderer is able to perform the discard and notify the
  // Browser.
  content::RenderProcessHost* process =
      GetWebContentsAt(0)->GetPrimaryMainFrame()->GetProcess();
  EXPECT_TRUE(process->IsInitializedAndNotDead());

  // Discard the tab and wait for the process to exit.
  WindowedRenderProcessHostExitObserver observer;
  EXPECT_EQ(LifecycleUnitState::ACTIVE, GetLifecycleUnitAt(0)->GetState());
  EXPECT_TRUE(GetLifecycleUnitAt(0)->Discard(LifecycleUnitDiscardReason::URGENT,
                                             /*resident_set_size_estimate=*/0));
  EXPECT_EQ(LifecycleUnitState::DISCARDED, GetLifecycleUnitAt(0)->GetState());
  observer.Wait();
  EXPECT_FALSE(process->IsInitializedAndNotDead());

  // Assert the Browser acknowledges and records the discard latency.
  EXPECT_TRUE(base::test::RunUntil([&] {
    return histogram_tester
               .GetAllSamples("Discarding.TabLifecycleUnit.DiscardLatency")
               .size() == 1;
  }));
}

IN_PROC_BROWSER_TEST_P(TabManagerTest, DiscardedTabHasNoProcess) {
  GURL test_page(chrome_test_utils::GetTestUrl(
      base::FilePath(), base::FilePath(FILE_PATH_LITERAL("simple.html"))));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page));
  content::WebContents* web_contents = tsm()->GetActiveWebContents();

  // The renderer process should be alive at this point.
  content::RenderProcessHost* process =
      web_contents->GetPrimaryMainFrame()->GetProcess();
  ASSERT_TRUE(process);
  EXPECT_TRUE(process->IsInitializedAndNotDead());
  EXPECT_NE(base::kNullProcessHandle, process->GetProcess().Handle());
  const int initial_renderer_id = process->GetDeprecatedID();

  // Discard the tab. This simulates a tab discard.
  TabLifecycleUnitExternal::FromWebContents(web_contents)
      ->DiscardTab(LifecycleUnitDiscardReason::URGENT);

  // Replacing the WebContents for the discard operation should result in
  // assignment of a new RenderProcessHost.
  if (!base::FeatureList::IsEnabled(features::kWebContentsDiscard)) {
    content::WebContents* new_web_contents = tsm()->GetActiveWebContents();
    EXPECT_NE(new_web_contents, web_contents);
    web_contents = new_web_contents;
    content::RenderProcessHost* new_process =
        web_contents->GetPrimaryMainFrame()->GetProcess();
    EXPECT_NE(new_process, process);
    EXPECT_NE(new_process->GetDeprecatedID(), initial_renderer_id);
    process = new_process;
  }

  // The renderer process should be dead after a discard.
  EXPECT_EQ(process, web_contents->GetPrimaryMainFrame()->GetProcess());
  EXPECT_FALSE(process->IsInitializedAndNotDead());
  EXPECT_EQ(base::kNullProcessHandle, process->GetProcess().Handle());

  // Here we simulate re-focussing the tab causing reload with navigation,
  // the navigation will reload the tab.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), test_page));

  // Reload should mean that the renderer process is alive now.
  EXPECT_EQ(process, web_contents->GetPrimaryMainFrame()->GetProcess());
  EXPECT_TRUE(process->IsInitializedAndNotDead());
  EXPECT_NE(base::kNullProcessHandle, process->GetProcess().Handle());
}

IN_PROC_BROWSER_TEST_P(TabManagerTest,
                       TabManagerWasDiscardedCrossSiteSubFrame) {
  const char kDiscardedStateJS[] = "window.document.wasDiscarded;";
  // Navigate to a page with a cross-site frame.
  content::SetupCrossSiteRedirector(embedded_test_server());
  ASSERT_TRUE(embedded_test_server()->Start());
  GURL main_url(
      embedded_test_server()->GetURL("a.com", "/iframe_cross_site.html"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  // Grab the original frames.
  content::WebContents* contents = tsm()->GetActiveWebContents();
  content::RenderFrameHost* main_frame = contents->GetPrimaryMainFrame();
  content::RenderFrameHost* child_frame = ChildFrameAt(main_frame, 0);
  ASSERT_TRUE(child_frame);

  // Sanity check that in this test page the main frame and the
  // subframe are cross-site.
  EXPECT_NE(main_frame->GetLastCommittedURL().DeprecatedGetOriginAsURL(),
            child_frame->GetLastCommittedURL().DeprecatedGetOriginAsURL());
  if (content::AreAllSitesIsolatedForTesting()) {
    EXPECT_NE(main_frame->GetSiteInstance(), child_frame->GetSiteInstance());
    EXPECT_NE(main_frame->GetProcess()->GetDeprecatedID(),
              child_frame->GetProcess()->GetDeprecatedID());
  }

  // document.wasDiscarded is false before discard, on main frame and child
  // frame.
  EXPECT_EQ(false, content::EvalJs(main_frame, kDiscardedStateJS));

  EXPECT_EQ(false, content::EvalJs(child_frame, kDiscardedStateJS));

  // Discard the tab. This simulates a tab discard.
  TabLifecycleUnitExternal::FromWebContents(contents)->DiscardTab(
      LifecycleUnitDiscardReason::URGENT);

  // Here we simulate re-focussing the tab causing reload with navigation,
  // the navigation will reload the tab.
  // TODO(panicker): Consider adding a test hook on LifecycleUnit when ready.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  // Re-assign pointers after discarding, as they've changed.
  contents = tsm()->GetActiveWebContents();
  main_frame = contents->GetPrimaryMainFrame();
  child_frame = ChildFrameAt(main_frame, 0);
  ASSERT_TRUE(child_frame);

  // document.wasDiscarded is true after discard, on mainframe and childframe.
  EXPECT_EQ(true, content::EvalJs(main_frame, kDiscardedStateJS));

  EXPECT_EQ(true, content::EvalJs(child_frame, kDiscardedStateJS));

  // Navigate the child frame, wasDiscarded is not set anymore.
  GURL childframe_url(embedded_test_server()->GetURL("b.com", "/title1.html"));
  EXPECT_TRUE(NavigateIframeToURL(contents, "frame1", childframe_url));
  EXPECT_EQ(false,
            content::EvalJs(ChildFrameAt(contents, 0), kDiscardedStateJS));

  // Navigate second child frame cross site.
  GURL second_childframe_url(
      embedded_test_server()->GetURL("d.com", "/title1.html"));
  EXPECT_TRUE(NavigateIframeToURL(contents, "frame2", second_childframe_url));
  EXPECT_EQ(false,
            content::EvalJs(ChildFrameAt(contents, 1), kDiscardedStateJS));

  // Navigate the main frame (same site) again, wasDiscarded is not set anymore.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));
  main_frame = contents->GetPrimaryMainFrame();
  EXPECT_EQ(false, content::EvalJs(main_frame, kDiscardedStateJS));

  // Go back in history and ensure wasDiscarded is still false.
  content::TestNavigationObserver observer(contents);
  contents->GetController().GoBack();
  observer.Wait();
  main_frame = contents->GetPrimaryMainFrame();
  EXPECT_EQ(false, content::EvalJs(main_frame, kDiscardedStateJS));
}

class TabManagerFencedFrameTest : public TabManagerTest {
 public:
  TabManagerFencedFrameTest()
      : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {
    https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
    https_server_.AddDefaultHandlers(GetChromeTestDataDir());
  }
  ~TabManagerFencedFrameTest() override = default;

 protected:
  net::EmbeddedTestServer& https_server() { return https_server_; }
  content::test::FencedFrameTestHelper& fenced_frame_test_helper() {
    return fenced_frame_test_helper_;
  }

 private:
  net::EmbeddedTestServer https_server_;
  content::test::FencedFrameTestHelper fenced_frame_test_helper_;
};

// Tests that `window.document.wasDiscarded` is updated for a fenced frame.
IN_PROC_BROWSER_TEST_P(TabManagerFencedFrameTest, TabManagerWasDiscarded) {
  const char kDiscardedStateJS[] = "window.document.wasDiscarded;";

  // Navigate to a page with a fenced frame.
  ASSERT_TRUE(https_server().Start());
  GURL main_url(
      https_server().GetURL("c.test", "/fenced_frames/basic_title.html"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  // Grab the original frames.
  content::WebContents* contents = tsm()->GetActiveWebContents();
  content::RenderFrameHost* primary_main_frame =
      contents->GetPrimaryMainFrame();

  content::RenderFrameHost* fenced_frame =
      fenced_frame_test_helper().GetMostRecentlyAddedFencedFrame(
          primary_main_frame);
  ASSERT_TRUE(fenced_frame);

  // document.wasDiscarded is false before discard, on a main frame and fenced
  // frame.
  EXPECT_EQ(false, content::EvalJs(primary_main_frame, kDiscardedStateJS));

  EXPECT_EQ(false, content::EvalJs(fenced_frame, kDiscardedStateJS));

  // Discard the tab. This simulates a tab discard.
  TabLifecycleUnitExternal::FromWebContents(contents)->DiscardTab(
      LifecycleUnitDiscardReason::URGENT);

  // Here we simulate re-focussing the tab causing reload with navigation,
  // the navigation will reload the tab.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), main_url));

  // Re-assign pointers after discarding, as they've changed.
  contents = tsm()->GetActiveWebContents();
  primary_main_frame = contents->GetPrimaryMainFrame();
  fenced_frame = fenced_frame_test_helper().GetMostRecentlyAddedFencedFrame(
      primary_main_frame);
  ASSERT_TRUE(fenced_frame);

  // document.wasDiscarded is true after discard, on a main frame and fenced
  // frame.
  EXPECT_EQ(true, content::EvalJs(primary_main_frame, kDiscardedStateJS));

  EXPECT_EQ(true, content::EvalJs(fenced_frame, kDiscardedStateJS));
}

// Do not run in debug or ASAN builds to avoid timeouts due to multiple
// navigations. https://crbug.com/40706359
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER)
#define MAYBE_DiscardTabsWithMinimizedWindow \
  DISABLED_DiscardTabsWithMinimizedWindow
#else
#define MAYBE_DiscardTabsWithMinimizedWindow DiscardTabsWithMinimizedWindow
#endif
IN_PROC_BROWSER_TEST_P(TabManagerTest, MAYBE_DiscardTabsWithMinimizedWindow) {
  // Do not override the focused TabStripModel.
  focused_tab_strip_model_override_.RunAndReset();

  // Minimized browser.
  EnsureTabsInBrowser(browser(), 2);
  browser()->GetWindow()->Minimize();

  for (int i = 0; i < 8; ++i) {
    UrgentDiscardTabImmediately();
  }

  base::RunLoop().RunUntilIdle();

  // Active tabs cannot be discarded.
  EXPECT_FALSE(
      IsTabDiscarded(browser()->GetTabStripModel()->GetWebContentsAt(0)));

  // Non-active tabs can be discarded.
  EXPECT_TRUE(
      IsTabDiscarded(browser()->GetTabStripModel()->GetWebContentsAt(1)));

  // Showing the browser again should reload the active tab.
  browser()->GetWindow()->Show();
  base::RunLoop().RunUntilIdle();
  EXPECT_FALSE(
      IsTabDiscarded(browser()->GetTabStripModel()->GetWebContentsAt(0)));
}

// Do not run in debug or ASAN builds to avoid timeouts due to multiple
// navigations. https://crbug.com/40706359
#if !defined(NDEBUG) || defined(ADDRESS_SANITIZER)
#define MAYBE_DiscardTabsWithOccludedWindow \
  DISABLED_DiscardTabsWithOccludedWindow
#else
#define MAYBE_DiscardTabsWithOccludedWindow DiscardTabsWithOccludedWindow
#endif
IN_PROC_BROWSER_TEST_P(TabManagerTest, MAYBE_DiscardTabsWithOccludedWindow) {
  // Occluded browser.
  EnsureTabsInBrowser(browser(), 2);
  browser()->GetWindow()->SetBounds(gfx::Rect(10, 10, 10, 10));
  // Other browser that covers the occluded browser.
  BrowserWindowInterface* const other_browser = CreateBrowserWithTabs(1);
  EXPECT_NE(other_browser, browser());
  other_browser->GetWindow()->SetBounds(gfx::Rect(0, 0, 100, 100));

  for (int i = 0; i < 3; ++i) {
    UrgentDiscardTabImmediately();
  }

  base::RunLoop().RunUntilIdle();

  EXPECT_FALSE(
      IsTabDiscarded(browser()->GetTabStripModel()->GetWebContentsAt(0)));

  // Non-active tabs can be discarded on all platforms.
  EXPECT_TRUE(
      IsTabDiscarded(browser()->GetTabStripModel()->GetWebContentsAt(1)));
}

INSTANTIATE_TEST_SUITE_P(
    ,
    TabManagerTest,
    ::testing::Values(false, true),
    [](const ::testing::TestParamInfo<TabManagerTest::ParamType>& info) {
      return info.param ? "RetainedWebContents" : "UnretainedWebContents";
    });

INSTANTIATE_TEST_SUITE_P(
    ,
    TabManagerTestWithTwoTabs,
    ::testing::Values(false, true),
    [](const ::testing::TestParamInfo<TabManagerTestWithTwoTabs::ParamType>&
           info) {
      return info.param ? "RetainedWebContents" : "UnretainedWebContents";
    });

INSTANTIATE_TEST_SUITE_P(
    ,
    TabManagerIgnoreWorkersTest,
    ::testing::Values(true),
    [](const ::testing::TestParamInfo<TabManagerTestWithTwoTabs::ParamType>&
           info) { return "RetainedWebContents"; });

INSTANTIATE_TEST_SUITE_P(
    ,
    TabManagerFencedFrameTest,
    ::testing::Values(false, true),
    [](const ::testing::TestParamInfo<TabManagerFencedFrameTest::ParamType>&
           info) {
      return info.param ? "RetainedWebContents" : "UnretainedWebContents";
    });
// Data race on Linux. http://crbug.com/41357022
// Flaky on Mac and Windows: https://crbug.com/41477172
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_MAC) || \
    BUILDFLAG(IS_WIN)
#define MAYBE_DiscardTabWithNonVisibleTabs DISABLED_DiscardTabWithNonVisibleTabs
#else
#define MAYBE_DiscardTabWithNonVisibleTabs DiscardTabWithNonVisibleTabs
#endif

// Verify that:
// - On ChromeOS, DiscardTab can discard every non-visible tab, but cannot
//   discard a visible tab.
// - On other platforms, DiscardTab can discard every tab that is not active in
//   its tab strip.
IN_PROC_BROWSER_TEST_P(TabManagerTest, MAYBE_DiscardTabWithNonVisibleTabs) {
  // Create 2 windows. Simulate the second window being hidden/not active.
  BrowserWindowInterface* browser1 = browser();

  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser1, GURL("https://www.example.com")));
  ui_test_utils::NavigateToURLWithDisposition(
      browser1, GURL("https://www.example.com"),
      WindowOpenDisposition::NEW_BACKGROUND_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  BrowserWindowInterface* browser2 = CreateBrowser(browser1->GetProfile());
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser2, GURL("https://www.example.com")));
  ui_test_utils::NavigateToURLWithDisposition(
      browser2, GURL("https://www.example.com"),
      WindowOpenDisposition::NEW_BACKGROUND_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  TabStripModel* tab_strip1 = browser1->GetTabStripModel();
  TabStripModel* tab_strip2 = browser2->GetTabStripModel();

  ASSERT_EQ(2, tab_strip1->count());
  ASSERT_EQ(2, tab_strip2->count());

  for (int i = 0; i < 4; ++i) {
    tab_manager()->DiscardTabByExtension(nullptr);
  }

  // Active tab in a visible window should not be discarded.
  EXPECT_FALSE(IsTabDiscarded(tab_strip1->GetWebContentsAt(0)));

  // Non-active tabs should be discarded.
  EXPECT_TRUE(IsTabDiscarded(tab_strip1->GetWebContentsAt(1)));
  EXPECT_TRUE(IsTabDiscarded(tab_strip2->GetWebContentsAt(1)));

#if BUILDFLAG(IS_CHROMEOS)
  // On ChromeOS, a non-visible tab should be discarded even if it's active in
  // its tab strip.
  EXPECT_TRUE(IsTabDiscarded(tab_strip2->GetWebContentsAt(0)));
#else
  // On other platforms, an active tab is never discarded, even if it's not
  // visible.
  EXPECT_FALSE(IsTabDiscarded(tab_strip2->GetWebContentsAt(0)));
#endif  // BUILDFLAG(IS_CHROMEOS)
}

}  // namespace resource_coordinator

#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) ||
        // BUILDFLAG(IS_CHROMEOS)
