// 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 "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/timer/timer.h"
#include "build/build_config.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/bookmarks/bookmark_model_factory.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/interstitials/security_interstitial_page_test_utils.h"
#include "chrome/browser/platform_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/ui/bookmarks/bookmark_bar_controller.h"
#include "chrome/browser/ui/bookmarks/bookmark_drag_drop.h"
#include "chrome/browser/ui/bookmarks/bookmark_tab_helper.h"
#include "chrome/browser/ui/bookmarks/bookmark_tab_helper_observer.h"
#include "chrome/browser/ui/bookmarks/bookmark_utils_desktop.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_element_identifiers.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/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/bookmarks/browser/url_and_title.h"
#include "components/bookmarks/test/bookmark_test_helpers.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#include "components/security_interstitials/content/security_interstitial_tab_helper.h"
#include "components/signin/public/base/signin_switches.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/prerender_test_util.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "ui/base/dragdrop/mojom/drag_drop_types.mojom-shared.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/base/interaction/element_tracker.h"
#include "ui/base/ui_base_features.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/views/controls/button/checkbox.h"
#include "ui/views/interaction/element_tracker_views.h"
#include "ui/views/test/button_test_api.h"
#include "ui/views/widget/any_widget_observer.h"
#include "ui/views/widget/widget.h"
#include "ui/views/widget/widget_delegate.h"
#include "ui/views/window/dialog_delegate.h"

using bookmarks::BookmarkModel;
using bookmarks::BookmarkNode;
using bookmarks::UrlAndTitle;

namespace {

const char kPersistBookmarkURL[] = "http://www.cnn.com/";
const char16_t kPersistBookmarkTitle[] = u"CNN";
const base::Time kPersistLastUsedTime =
    base::Time() + base::Days(7) + base::Hours(2) + base::Minutes(55) +
    base::Seconds(24) + base::Milliseconds(133);

}  // namespace

class TestBookmarkTabHelperObserver : public BookmarkTabHelperObserver {
 public:
  explicit TestBookmarkTabHelperObserver(BookmarkTabHelper* helper) {
    observation_.Observe(helper);
  }

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

  ~TestBookmarkTabHelperObserver() override = default;

  void URLStarredChanged(content::WebContents*, bool starred) override {
    starred_ = starred;
  }
  bool is_starred() const { return starred_; }

 private:
  base::ScopedObservation<BookmarkTabHelper, BookmarkTabHelperObserver>
      observation_{this};

  bool starred_ = false;
};

class BookmarkBrowsertest : public InProcessBrowserTest {
 public:
  BookmarkBrowsertest() {
    feature_list_.InitWithFeatures(
        /*enabled_features=*/{switches::kSyncEnableBookmarksInTransportMode},
        /*disabled_features=*/{
#if BUILDFLAG(IS_WIN)
            // This needs to be disabled so that animations are guaranteed to
            // work.
            features::kApplyNativeOcclusionToCompositor
#endif
        });
  }

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

  bool IsVisible() {
    return BookmarkBarController::From(browser())->bookmark_bar_state() ==
           BookmarkBar::SHOW;
  }

  static void CheckAnimation(BrowserWindowInterface* browser,
                             base::RunLoop* loop) {
    if (!BrowserView::GetBrowserViewForBrowser(browser)
             ->IsBookmarkBarAnimating()) {
      loop->Quit();
    }
  }

  base::TimeDelta WaitForBookmarkBarAnimationToFinish() {
    base::Time start(base::Time::Now());
    {
      base::RunLoop loop;
      base::RepeatingTimer timer;
      timer.Start(FROM_HERE, base::Milliseconds(15),
                  base::BindRepeating(&CheckAnimation, browser(), &loop));
      loop.Run();
    }
    return base::Time::Now() - start;
  }

  BookmarkModel* WaitForBookmarkModel(Profile* profile) {
    BookmarkModel* bookmark_model =
        BookmarkModelFactory::GetForBrowserContext(profile);
    bookmarks::test::WaitForBookmarkModelToLoad(bookmark_model);
    return bookmark_model;
  }

  base::HistogramTester* histogram_tester() { return &histogram_tester_; }

 private:
  base::test::ScopedFeatureList feature_list_;

  // We make the histogram tester a member field to make sure it starts
  // recording as early as possible.
  base::HistogramTester histogram_tester_;
};

// Test of bookmark bar toggling, visibility, and animation.
IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, BookmarkBarVisibleWait) {
  ASSERT_FALSE(IsVisible());
  chrome::ExecuteCommand(browser(), IDC_SHOW_BOOKMARK_BAR);
  base::TimeDelta delay = WaitForBookmarkBarAnimationToFinish();
  LOG(INFO) << "Took " << delay.InMilliseconds() << " ms to show bookmark bar";
  ASSERT_TRUE(IsVisible());
  chrome::ExecuteCommand(browser(), IDC_SHOW_BOOKMARK_BAR);
  delay = WaitForBookmarkBarAnimationToFinish();
  LOG(INFO) << "Took " << delay.InMilliseconds() << " ms to hide bookmark bar";
  ASSERT_FALSE(IsVisible());
}

// Verify that bookmarks persist browser restart.
IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, PRE_Persist) {
  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());

  const BookmarkNode* node = bookmarks::AddIfNotBookmarked(
      bookmark_model, GURL(kPersistBookmarkURL), kPersistBookmarkTitle);
  bookmark_model->UpdateLastUsedTime(node, kPersistLastUsedTime,
                                     /*just_opened=*/true);
}

#if BUILDFLAG(IS_WIN)
// TODO(crbug.com/41443454): The test fails on Windows.
#define MAYBE_Persist DISABLED_Persist
#else
#define MAYBE_Persist Persist
#endif

IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, MAYBE_Persist) {
  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());

  GURL url(kPersistBookmarkURL);
  std::vector<raw_ptr<const BookmarkNode, VectorExperimental>> nodes =
      bookmark_model->GetNodesByURL(url);

  ASSERT_EQ(1u, nodes.size());
  ASSERT_EQ(url, nodes[0]->url());
  ASSERT_EQ(kPersistBookmarkTitle, nodes[0]->GetTitledUrlNodeTitle());
  EXPECT_EQ(kPersistLastUsedTime, nodes[0]->date_last_used());
}

#if !BUILDFLAG(IS_CHROMEOS)  // No multi-profile on ChromeOS.

// Sanity check that bookmarks from different profiles are separate.
IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, MultiProfile) {
  BookmarkModel* bookmark_model1 =
      WaitForBookmarkModel(browser()->GetProfile());

  base::RunLoop run_loop;
  Profile* profile2 = nullptr;
  g_browser_process->profile_manager()->CreateMultiProfileAsync(
      u"New Profile", 0, false,
      base::BindLambdaForTesting([&](Profile* profile) {
        if (profile) {
          profile2 = profile;
          run_loop.Quit();
        }
      }));
  run_loop.Run();
  BookmarkModel* bookmark_model2 = WaitForBookmarkModel(profile2);

  bookmarks::AddIfNotBookmarked(bookmark_model1, GURL(kPersistBookmarkURL),
                                kPersistBookmarkTitle);

  ASSERT_EQ(1u, bookmark_model1->GetUniqueUrls().size());
  ASSERT_TRUE(bookmark_model2->GetUniqueUrls().empty());
}

#endif

// Sanity check that bookmarks from Incognito mode persist Incognito restart.
IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, IncognitoPersistence) {
  BrowserWindowInterface* incognito_browser = CreateIncognitoBrowser();
  BookmarkModel* bookmark_model =
      WaitForBookmarkModel(incognito_browser->GetProfile());

  // Add bookmark for Incognito and ensure it is added.
  bookmarks::AddIfNotBookmarked(bookmark_model, GURL(kPersistBookmarkURL),
                                kPersistBookmarkTitle);

  ASSERT_EQ(1u, bookmark_model->GetUniqueUrls().size());

  // Restart Incognito, and check again.
  CloseBrowserSynchronously(incognito_browser);
  incognito_browser = CreateIncognitoBrowser();
  bookmark_model = WaitForBookmarkModel(incognito_browser->GetProfile());
  ASSERT_EQ(1u, bookmark_model->GetUniqueUrls().size());

  // Ensure it is also available in regular mode.
  bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  ASSERT_EQ(1u, bookmark_model->GetUniqueUrls().size());
}

// Regression for crash caused by opening folder as a group in an incognito
// window when the folder contains URLs that cannot be displayed in incognito.
// See discussion starting at crbug.com/40786567#comment16
IN_PROC_BROWSER_TEST_F(
    BookmarkBrowsertest,
    OpenFolderAsGroupInIncognitoWhenBookmarksCantOpenInIncognito) {
  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  const BookmarkNode* const folder = bookmark_model->AddFolder(
      bookmark_model->bookmark_bar_node(), 0, u"Folder");
  const BookmarkNode* const page1 = bookmark_model->AddURL(
      folder, 0, u"Extensions", GURL(chrome::kChromeUIExtensionsURL));
  const BookmarkNode* const page2 = bookmark_model->AddURL(
      folder, 1, u"Settings", GURL(chrome::kChromeUISettingsURL));

  BrowserWindowInterface* incognito_browser = CreateIncognitoBrowser();
  BookmarkModel* incognito_model =
      WaitForBookmarkModel(incognito_browser->GetProfile());
  ASSERT_FALSE(incognito_model->root_node()->children().empty());
  ASSERT_TRUE(incognito_model->root_node()->children()[0]->is_folder());
  BookmarkNode* const incognito_folder =
      incognito_model->bookmark_bar_node()->children()[0].get();
  ASSERT_EQ(2U, incognito_folder->children().size());
  EXPECT_EQ(page1->url(), incognito_folder->children()[0]->url());
  EXPECT_EQ(page2->url(), incognito_folder->children()[1]->url());

  const int browser_tabs = browser()->GetTabStripModel()->count();
  const int incognito_tabs = incognito_browser->GetTabStripModel()->count();

  bookmarks::OpenAllIfAllowed(incognito_browser, {incognito_folder},
                              WindowOpenDisposition::NEW_BACKGROUND_TAB,
                              bookmarks::OpenAllBookmarksContext::kInGroup);

  EXPECT_EQ(incognito_tabs, incognito_browser->GetTabStripModel()->count());
  EXPECT_EQ(browser_tabs + 2, browser()->GetTabStripModel()->count());
}

IN_PROC_BROWSER_TEST_F(
    BookmarkBrowsertest,
    OpenBookmarkInSplitInIncognitoWhenBookmarkCantOpenInIncognito) {
  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  bookmark_model->AddURL(bookmark_model->bookmark_bar_node(), 0, u"Settings",
                         GURL(chrome::kChromeUISettingsURL));

  BrowserWindowInterface* incognito_browser = CreateIncognitoBrowser();
  BookmarkModel* incognito_model =
      WaitForBookmarkModel(incognito_browser->GetProfile());
  ASSERT_FALSE(incognito_model->bookmark_bar_node()->children().empty());
  BookmarkNode* const incognito_page =
      incognito_model->bookmark_bar_node()->children()[0].get();

  const int browser_tabs = browser()->GetTabStripModel()->count();
  const int incognito_tabs = incognito_browser->GetTabStripModel()->count();

  bookmarks::OpenAllIfAllowed(incognito_browser, {incognito_page},
                              WindowOpenDisposition::NEW_BACKGROUND_TAB,
                              bookmarks::OpenAllBookmarksContext::kInSplit);

  EXPECT_EQ(incognito_tabs, incognito_browser->GetTabStripModel()->count());
  EXPECT_EQ(browser_tabs + 1, browser()->GetTabStripModel()->count());
}


IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, OpenAllBookmarks) {
  BrowserWindowInterface* regular_browser = browser();
  BookmarkModel* bookmark_model =
      WaitForBookmarkModel(regular_browser->GetProfile());
  const BookmarkNode* const bbar = bookmark_model->bookmark_bar_node();
  ASSERT_TRUE(bbar->children().empty());

  BrowserWindowInterface* incognito_browser = CreateIncognitoBrowser();
  BookmarkModel* incognito_model =
      WaitForBookmarkModel(incognito_browser->GetProfile());
  const BookmarkNode* const incognito_bbar =
      incognito_model->bookmark_bar_node();

  auto close_all_tabs_except_first = [](BrowserWindowInterface* browser) {
    int num_tabs = browser->GetTabStripModel()->count();
    for (int i = 0; i < num_tabs - 1; ++i) {
      browser->GetTabStripModel()->CloseWebContentsAt(num_tabs - 1 - i, 0);
    }
    EXPECT_EQ(1, browser->GetTabStripModel()->count());
  };

  auto open_urls_and_test = [&regular_browser, &incognito_browser, &bbar,
                             &close_all_tabs_except_first, this]() {
    // open all in new tab from regular browser
    {
      close_all_tabs_except_first(regular_browser);
      close_all_tabs_except_first(incognito_browser);
      bookmarks::OpenAllIfAllowed(regular_browser, {bbar},
                                  WindowOpenDisposition::NEW_BACKGROUND_TAB);
      int num_tabs_regular = regular_browser->tab_strip_model()->count();
      int num_tabs_incognito = incognito_browser->tab_strip_model()->count();
      EXPECT_EQ(num_tabs_regular, 5);
      EXPECT_EQ(num_tabs_incognito, 1);
    }

    // open all in a new window from regular browser
    {
      close_all_tabs_except_first(regular_browser);
      close_all_tabs_except_first(incognito_browser);
      bookmarks::OpenAllIfAllowed(regular_browser, {bbar},
                                  WindowOpenDisposition::NEW_WINDOW);
      BrowserWindowInterface* regular_browser2 = nullptr;
      ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
          [&](BrowserWindowInterface* browser) {
            if (browser != incognito_browser && browser != regular_browser) {
              regular_browser2 = browser;
            }
            return !regular_browser2;
          });
      // new browser needs to be opened
      EXPECT_NE(regular_browser2, nullptr);
      int num_tabs_regular = regular_browser->tab_strip_model()->count();
      int num_tabs_regular2 = regular_browser2->GetTabStripModel()->count();
      EXPECT_EQ(num_tabs_regular, 1);
      EXPECT_EQ(num_tabs_regular2, 4);
      CloseBrowserSynchronously(regular_browser2);
    }

    // open all in a new incognito window from regular browser
    {
      close_all_tabs_except_first(regular_browser);
      close_all_tabs_except_first(incognito_browser);
      bookmarks::OpenAllIfAllowed(regular_browser, {bbar},
                                  WindowOpenDisposition::OFF_THE_RECORD);
      int num_tabs_incognito = incognito_browser->tab_strip_model()->count();
      EXPECT_EQ(num_tabs_incognito, 3);
    }
  };

  auto open_urls_from_incognito_and_test = [&regular_browser,
                                            &incognito_browser, &incognito_bbar,
                                            &close_all_tabs_except_first,
                                            this]() {
    // open all in new tab from incognito
    {
      close_all_tabs_except_first(regular_browser);
      close_all_tabs_except_first(incognito_browser);
      bookmarks::OpenAllIfAllowed(incognito_browser, {incognito_bbar},
                                  WindowOpenDisposition::NEW_BACKGROUND_TAB);
      int num_tabs_regular = regular_browser->tab_strip_model()->count();
      int num_tabs_incognito = incognito_browser->tab_strip_model()->count();
      EXPECT_EQ(num_tabs_regular, 3);
      EXPECT_EQ(num_tabs_incognito, 3);
    }

    // open all in new window from incognito
    {
      close_all_tabs_except_first(regular_browser);
      close_all_tabs_except_first(incognito_browser);
      bookmarks::OpenAllIfAllowed(incognito_browser, {incognito_bbar},
                                  WindowOpenDisposition::NEW_WINDOW);
      BrowserWindowInterface* incognito_browser2 = nullptr;
      ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
          [&](BrowserWindowInterface* browser) {
            if (browser != incognito_browser && browser != regular_browser) {
              incognito_browser2 = browser;
            }
            return !incognito_browser2;
          });
      // new browser needs to be opened
      EXPECT_NE(incognito_browser2, nullptr);
      int num_tabs_regular = regular_browser->tab_strip_model()->count();
      int num_tabs_incognito = incognito_browser->tab_strip_model()->count();
      int num_tabs_incognito2 = incognito_browser2->GetTabStripModel()->count();
      EXPECT_EQ(num_tabs_regular, 3);
      EXPECT_EQ(num_tabs_incognito, 1);
      EXPECT_EQ(num_tabs_incognito2, 2);
      CloseBrowserSynchronously(incognito_browser2);
    }
  };

  {
    // Bookmark 4 pages, with the first and third one not being able to be
    // opened in incognito mode
    bookmark_model->AddURL(bbar, 0, u"Settings",
                           GURL(chrome::kChromeUISettingsURL));
    bookmark_model->AddURL(bbar, 1, u"Google", GURL("http://www.google.com"));
    bookmark_model->AddURL(bbar, 2, u"Extensions",
                           GURL(chrome::kChromeUIExtensionsURL));
    bookmark_model->AddURL(bbar, 3, u"Gmail", GURL("http://mail.google.com"));
    open_urls_and_test();
    open_urls_from_incognito_and_test();
    bookmark_model->RemoveAllUserBookmarks(FROM_HERE);
  }
  {
    // Bookmark 4 pages, with the second and fourth one not being able to be
    // opened in incognito mode
    bookmark_model->AddURL(bbar, 0, u"Google", GURL("http://www.google.com"));
    bookmark_model->AddURL(bbar, 1, u"Settings",
                           GURL(chrome::kChromeUISettingsURL));
    bookmark_model->AddURL(bbar, 2, u"Gmail", GURL("http://mail.google.com"));
    bookmark_model->AddURL(bbar, 3, u"Extensions",
                           GURL(chrome::kChromeUIExtensionsURL));
    open_urls_and_test();
    open_urls_from_incognito_and_test();
    bookmark_model->RemoveAllUserBookmarks(FROM_HERE);
  }
}

IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, OpenAllFiltersJavascriptURLs) {
  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  const BookmarkNode* const folder = bookmark_model->AddFolder(
      bookmark_model->bookmark_bar_node(), 0, u"Folder");

  // Add a normal URL and a javascript: URL.
  bookmark_model->AddURL(folder, 0, u"Normal URL",
                         GURL("https://www.example.com"));
  bookmark_model->AddURL(folder, 1, u"Script URL", GURL("javascript:alert()"));

  ASSERT_EQ(1, browser()->GetTabStripModel()->count());

  bookmarks::OpenAllIfAllowed(browser(), {folder},
                              WindowOpenDisposition::NEW_BACKGROUND_TAB,
                              bookmarks::OpenAllBookmarksContext::kNone);
  EXPECT_EQ(2, browser()->GetTabStripModel()->count());
  EXPECT_EQ(
      GURL("https://www.example.com"),
      browser()->GetTabStripModel()->GetWebContentsAt(1)->GetVisibleURL());
}

IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest,
                       HideStarOnNonbookmarkedInterstitial) {
  // Start an HTTPS server with a certificate error.
  net::EmbeddedTestServer https_server(net::EmbeddedTestServer::TYPE_HTTPS);
  https_server.SetSSLConfig(net::EmbeddedTestServer::CERT_MISMATCHED_NAME);
  https_server.ServeFilesFromSourceDirectory(GetChromeTestDataDir());
  ASSERT_TRUE(https_server.Start());
  ASSERT_TRUE(embedded_test_server()->Start());

  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  GURL bookmark_url = embedded_test_server()->GetURL("example.test", "/");
  bookmarks::AddIfNotBookmarked(bookmark_model, bookmark_url, u"Bookmark");

  content::WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  BookmarkTabHelper* tab_helper =
      BookmarkTabHelper::FromWebContents(web_contents);
  TestBookmarkTabHelperObserver bookmark_observer(tab_helper);

  // Go to a bookmarked url. Bookmark star should show.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), bookmark_url));
  EXPECT_FALSE(
      chrome_browser_interstitials::IsShowingInterstitial(web_contents));
  EXPECT_TRUE(bookmark_observer.is_starred());
  // Now go to a non-bookmarked url which triggers an SSL warning. Bookmark
  // star should disappear.
  GURL error_url = https_server.GetURL("/");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), error_url));
  web_contents = browser()->GetTabStripModel()->GetActiveWebContents();
  EXPECT_TRUE(
      chrome_browser_interstitials::IsShowingInterstitial(web_contents));
  EXPECT_FALSE(bookmark_observer.is_starred());
}

// Provides coverage for the Bookmark Manager bookmark drag and drag image
// generation for dragging a single bookmark.
IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, DragSingleBookmark) {
  BookmarkModel* model = WaitForBookmarkModel(browser()->GetProfile());
  const std::u16string page_title(u"foo");
  const GURL page_url("http://www.google.com");
  const BookmarkNode* root = model->bookmark_bar_node();
  const BookmarkNode* node = model->AddURL(root, 0, page_title, page_url);
  const gfx::Point expected_point(100, 100);

  auto run_loop = std::make_unique<base::RunLoop>();

  chrome::DoBookmarkDragCallback cb = base::BindLambdaForTesting(
      [&run_loop, page_title, page_url, expected_point](
          std::unique_ptr<ui::OSExchangeData> drag_data,
          gfx::NativeView native_view, ui::mojom::DragEventSource source,
          gfx::Point point, int operation) {
        std::vector<ui::ClipboardUrlInfo> url_infos = drag_data->GetURLs(
            ui::FilenameToURLPolicy::DO_NOT_CONVERT_FILENAMES);
        ASSERT_FALSE(url_infos.empty());
        EXPECT_EQ(page_url, url_infos.front().url);
        EXPECT_EQ(page_title, url_infos.front().title);
#if !BUILDFLAG(IS_WIN)
        // On Windows, GetDragImage() is a NOTREACHED() as the Windows
        // implementation of OSExchangeData just sets the drag image on the OS
        // API. https://crbug.com/41419592
        EXPECT_FALSE(drag_data->provider().GetDragImage().isNull());
#endif
        EXPECT_EQ(expected_point, point);
        run_loop->Quit();
      });

  constexpr int kDragNodeIndex = 0;
  chrome::DragBookmarksForTest(
      browser()->GetProfile(),
      {{node},
       kDragNodeIndex,
       browser()->GetTabStripModel()->GetActiveWebContents(),
       ui::mojom::DragEventSource::kMouse,
       expected_point},
      std::move(cb));

  run_loop->Run();
}

// A favicon update during drag shouldn't trigger the drag flow again. The test
// passes if the favicon update does not cause a crash. (see
// https://crbug.com/40865326)
IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, FaviconChangeDuringBookmarkDrag) {
  BookmarkModel* model = WaitForBookmarkModel(browser()->GetProfile());
  const std::u16string kPageTitle(u"foo");
  const GURL kPageUrl("http://www.google.com");
  const GURL kFaviconUrl("http://www.google.com/favicon.ico");
  const BookmarkNode* root = model->bookmark_bar_node();
  const BookmarkNode* node = model->AddURL(root, 0, kPageTitle, kPageUrl);
  constexpr gfx::Point kExpectedPoint(100, 100);

  auto run_loop = std::make_unique<base::RunLoop>();

  chrome::DoBookmarkDragCallback cb = base::BindLambdaForTesting(
      [&run_loop, model, kPageUrl, kFaviconUrl](
          std::unique_ptr<ui::OSExchangeData> drag_data,
          gfx::NativeView native_view, ui::mojom::DragEventSource source,
          gfx::Point point, int operation) {
        // Simulate a favicon change during the drag operation.
        model->OnFaviconsChanged({kPageUrl}, kFaviconUrl);
        run_loop->Quit();
      });

  constexpr int kDragNodeIndex = 0;
  chrome::DragBookmarksForTest(
      browser()->GetProfile(),
      {{node},
       kDragNodeIndex,
       browser()->GetTabStripModel()->GetActiveWebContents(),
       ui::mojom::DragEventSource::kMouse,
       kExpectedPoint},
      std::move(cb));

  run_loop->Run();
}

// Provides coverage for the Bookmark Manager bookmark drag and drag image
// generation for dragging multiple bookmarks.
IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, DragMultipleBookmarks) {
  BookmarkModel* model = WaitForBookmarkModel(browser()->GetProfile());
  const std::u16string page_title = u"foo";
  const GURL page_url("http://www.google.com");
  const BookmarkNode* root = model->bookmark_bar_node();
  const BookmarkNode* node1 = model->AddURL(root, 0, page_title, page_url);
  const BookmarkNode* node2 = model->AddFolder(root, 0, page_title);
  const gfx::Point expected_point(100, 100);

  auto run_loop = std::make_unique<base::RunLoop>();

  chrome::DoBookmarkDragCallback cb = base::BindLambdaForTesting(
      [&run_loop, expected_point, page_title, page_url](
          std::unique_ptr<ui::OSExchangeData> drag_data,
          gfx::NativeView native_view, ui::mojom::DragEventSource source,
          gfx::Point point, int operation) {
        const std::vector<ui::ClipboardUrlInfo> url_infos = drag_data->GetURLs(
            ui::FilenameToURLPolicy::DO_NOT_CONVERT_FILENAMES);
        ASSERT_FALSE(url_infos.empty());

        // The bookmarks are added in order, and the first is retrieved, so
        // expect the values from the first bookmark.
        // TODO(http://crbug.com/41011768): test the bookmark folder.
        EXPECT_EQ(page_title, url_infos.front().title);
        EXPECT_EQ(page_url, url_infos.front().url);
#if !BUILDFLAG(IS_WIN)
        // On Windows, GetDragImage() is a NOTREACHED() as the Windows
        // implementation of OSExchangeData just sets the drag image on the OS
        // API. https://crbug.com/41419592
        EXPECT_FALSE(drag_data->provider().GetDragImage().isNull());
#endif
        EXPECT_EQ(expected_point, point);
        run_loop->Quit();
      });

  constexpr int kDragNodeIndex = 1;
  chrome::DragBookmarksForTest(
      browser()->GetProfile(),
      {
          {node1, node2},
          kDragNodeIndex,
          browser()->GetTabStripModel()->GetActiveWebContents(),
          ui::mojom::DragEventSource::kMouse,
          expected_point,
      },
      std::move(cb));

  run_loop->Run();
}

IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, PRE_EmitUmaForTimeMetrics) {
  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  const BookmarkNode* parent = bookmarks::GetParentForNewNodes(bookmark_model);
  const BookmarkNode* other_parent =
      bookmark_model->AddFolder(parent, 0, u"Folder");

  // Add one bookmark with a unique URL, two other bookmarks with a shared URL,
  // and three more with another shared URL.
  bookmark_model->AddURL(parent, parent->children().size(), u"title1",
                         GURL("http://a.com"));
  bookmark_model->AddURL(parent, parent->children().size(), u"title2",
                         GURL("http://b.com"));
  bookmark_model->AddURL(parent, parent->children().size(), u"title3",
                         GURL("http://b.com"));
  bookmark_model->AddURL(parent, parent->children().size(), u"title4",
                         GURL("http://c.com"));
  bookmark_model->AddURL(parent, parent->children().size(), u"title5",
                         GURL("http://c.com"));
  bookmark_model->AddURL(parent, parent->children().size(), u"title5",
                         GURL("http://c.com"));
  bookmark_model->AddURL(other_parent, other_parent->children().size(),
                         u"title5", GURL("http://c.com"));
}

IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, EmitUmaForTimeMetrics) {
  WaitForBookmarkModel(browser()->GetProfile());

  // The total number of bookmarks is 7, but it gets rounded down due to
  // bucketing.
  ASSERT_THAT(
      histogram_tester()->GetAllSamples("Bookmarks.Count.OnProfileLoad3"),
      testing::ElementsAre(base::Bucket(/*min=*/6, /*count=*/1)));

  EXPECT_THAT(histogram_tester()->GetAllSamples(
                  "Bookmarks.Times.OnProfileLoad.TimeSinceAdded3"),
              testing::ElementsAre(base::Bucket(/*min=*/0, /*count=*/1)));
}

IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, PRE_EmitUmaForMostRecentlyUsed) {
  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  BookmarkNode* parent = const_cast<BookmarkNode*>(bookmark_model->AddFolder(
      bookmarks::GetParentForNewNodes(bookmark_model), 0, u"Test Folder"));
  parent->set_date_added(base::Time::Now() - base::Days(3));

  BookmarkNode* node = const_cast<BookmarkNode*>(bookmark_model->AddURL(
      parent, parent->children().size(), u"title1", GURL("http://a.com")));
  node->set_date_added(base::Time::Now() - base::Days(2));
  node->set_date_last_used(base::Time::Now() - base::Days(1));

  // This shouldn't count towards metrics because there's another node which
  // is more recently saved/used.
  node = const_cast<BookmarkNode*>(bookmark_model->AddURL(
      parent, parent->children().size(), u"title1", GURL("http://a.com")));
  node->set_date_added(base::Time::Now() - base::Days(3));
  node->set_date_last_used(base::Time::Now() - base::Days(3));
}

IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, EmitUmaForMostRecentlyUsed) {
  WaitForBookmarkModel(browser()->GetProfile());

  EXPECT_THAT(
      histogram_tester()->GetAllSamples(
          "Bookmarks.Times.OnProfileLoad.MostRecentlyUsedBookmarkInDays"),
      testing::ElementsAre(base::Bucket(/*min=*/1, /*count=*/1)));

  EXPECT_THAT(
      histogram_tester()->GetAllSamples(
          "Bookmarks.Times.OnProfileLoad.MostRecentlySavedBookmarkInDays"),
      testing::ElementsAre(base::Bucket(/*min=*/2, /*count=*/1)));
  EXPECT_THAT(
      histogram_tester()->GetAllSamples(
          "Bookmarks.Times.OnProfileLoad.MostRecentlyAddedFolderInDays"),
      testing::ElementsAre(base::Bucket(/*min=*/3, /*count=*/1)));
}

IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest,
                       EmitUmaForMostRecentlyUsed_NoBookmarks) {
  WaitForBookmarkModel(browser()->GetProfile());

  EXPECT_THAT(
      histogram_tester()->GetAllSamples(
          "Bookmarks.Times.OnProfileLoad.MostRecentlyUsedBookmarkInDays"),
      testing::ElementsAre());

  EXPECT_THAT(
      histogram_tester()->GetAllSamples(
          "Bookmarks.Times.OnProfileLoad.MostRecentlySavedBookmarkInDays"),
      testing::ElementsAre());
  EXPECT_THAT(
      histogram_tester()->GetAllSamples(
          "Bookmarks.Times.OnProfileLoad.MostRecentlyAddedFolderInDays"),
      testing::ElementsAre());
}

// Test that the bookmark star state updates in response to same document
// navigations that change the URL
IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, SameDocumentNavigation) {
  ASSERT_TRUE(embedded_test_server()->Start());

  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  GURL bookmark_url = embedded_test_server()->GetURL("/title1.html");
  bookmarks::AddIfNotBookmarked(bookmark_model, bookmark_url, u"Bookmark");

  content::WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  BookmarkTabHelper* tab_helper =
      BookmarkTabHelper::FromWebContents(web_contents);
  TestBookmarkTabHelperObserver bookmark_observer(tab_helper);

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), bookmark_url));
  EXPECT_TRUE(bookmark_observer.is_starred());

  GURL same_document_url = embedded_test_server()->GetURL("/title1.html#test");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), same_document_url));
  EXPECT_FALSE(bookmark_observer.is_starred());
}

IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest,
                       DifferentDocumentNavigationWithoutFinishing) {
  ASSERT_TRUE(embedded_test_server()->Start());

  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  GURL bookmark_url = embedded_test_server()->GetURL("/title1.html");
  bookmarks::AddIfNotBookmarked(bookmark_model, bookmark_url, u"Bookmark");

  content::WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  BookmarkTabHelper* tab_helper =
      BookmarkTabHelper::FromWebContents(web_contents);
  TestBookmarkTabHelperObserver bookmark_observer(tab_helper);

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), bookmark_url));
  EXPECT_TRUE(bookmark_observer.is_starred());

  // Navigate to the second page and check that the bookmark is not starred even
  // when the navigation is not finished.
  GURL different_document_url = embedded_test_server()->GetURL("/title2.html");
  content::TestNavigationManager manager(web_contents, different_document_url);
  web_contents->GetController().LoadURL(
      different_document_url, content::Referrer(), ui::PAGE_TRANSITION_TYPED,
      std::string());
  EXPECT_TRUE(manager.WaitForRequestStart());
  EXPECT_FALSE(manager.was_committed());
  EXPECT_EQ(web_contents->GetVisibleURL(), different_document_url);
  EXPECT_EQ(web_contents->GetLastCommittedURL(), bookmark_url);
  EXPECT_FALSE(bookmark_observer.is_starred());
}

IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest, NonCommitURLNavigation) {
  ASSERT_TRUE(embedded_test_server()->Start());

  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  GURL bookmark_url = embedded_test_server()->GetURL("/title1.html");
  bookmarks::AddIfNotBookmarked(bookmark_model, bookmark_url, u"Bookmark");

  content::WebContents* web_contents =
      browser()->GetTabStripModel()->GetActiveWebContents();
  BookmarkTabHelper* tab_helper =
      BookmarkTabHelper::FromWebContents(web_contents);
  TestBookmarkTabHelperObserver bookmark_observer(tab_helper);

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), bookmark_url));
  EXPECT_TRUE(bookmark_observer.is_starred());

  const GURL non_commit_url = embedded_test_server()->GetURL("/page204.html");
  content::TestNavigationManager manager(web_contents, non_commit_url);
  web_contents->GetController().LoadURL(non_commit_url, content::Referrer(),
                                        ui::PAGE_TRANSITION_TYPED,
                                        std::string());
  EXPECT_TRUE(manager.WaitForRequestStart());
  EXPECT_FALSE(manager.was_committed());
  EXPECT_EQ(web_contents->GetVisibleURL(), non_commit_url);
  EXPECT_EQ(web_contents->GetLastCommittedURL(), bookmark_url);
  EXPECT_FALSE(bookmark_observer.is_starred());

  // Since the navigation did not commit, the last committed URL becomes the
  // visible URL again, so the starred state should be restored.
  ASSERT_TRUE(manager.WaitForNavigationFinished());
  EXPECT_FALSE(manager.was_committed());
  EXPECT_TRUE(bookmark_observer.is_starred());
}

IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest,
                       BookmarkCurrentTab_WithoutAccountNodes) {
  ASSERT_TRUE(embedded_test_server()->Start());
  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());

  // If no folders have been modified, bookmarks are saved to other bookmarks by
  // default.
  ASSERT_EQ(bookmark_model->other_node(),
            bookmarks::GetParentForNewNodes(bookmark_model));

  ASSERT_TRUE(chrome::ExecuteCommand(browser(), IDC_BOOKMARK_THIS_TAB));
  EXPECT_THAT(1u, bookmark_model->other_node()->children().size());

  // Add a bookmark to the local bookmark bar node, so that it becomes the most
  // recently modified one.
  bookmark_model->AddURL(bookmark_model->bookmark_bar_node(), 0, u"Title",
                         GURL("http://google.com"));
  ASSERT_EQ(bookmark_model->bookmark_bar_node(),
            bookmarks::GetParentForNewNodes(bookmark_model));

  // After the bookmarks bar was modified, it should be the new default save
  // location.
  ASSERT_TRUE(chrome::ExecuteCommand(browser(), IDC_BOOKMARK_THIS_TAB));
  EXPECT_THAT(1u, bookmark_model->bookmark_bar_node()->children().size());
}

// Account nodes don't exist on ChromeOS, so this test does not apply.
#if !BUILDFLAG(IS_CHROMEOS)
IN_PROC_BROWSER_TEST_F(BookmarkBrowsertest,
                       BookmarkCurrentTab_WithAccountNodes) {
  ASSERT_TRUE(embedded_test_server()->Start());
  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  bookmark_model->CreateAccountPermanentFolders();

  // If no folders have been modified, bookmarks are saved to account other
  // bookmarks by default.
  ASSERT_EQ(bookmark_model->account_other_node(),
            bookmarks::GetParentForNewNodes(bookmark_model));

  ASSERT_TRUE(chrome::ExecuteCommand(browser(), IDC_BOOKMARK_THIS_TAB));
  EXPECT_THAT(1u, bookmark_model->account_other_node()->children().size());

  // Add a bookmark to the local bookmark bar node, so that it becomes the most
  // recently modified one.
  bookmark_model->AddURL(bookmark_model->bookmark_bar_node(), 0, u"Title",
                         GURL("http://google.com"));
  ASSERT_EQ(bookmark_model->bookmark_bar_node(),
            bookmarks::GetParentForNewNodes(bookmark_model));

  // After the bookmarks bar was modified, it should be the new default save
  // location.
  ASSERT_TRUE(chrome::ExecuteCommand(browser(), IDC_BOOKMARK_THIS_TAB));
  EXPECT_THAT(1u, bookmark_model->bookmark_bar_node()->children().size());
}
#endif  // !BUILDFLAG(IS_CHROMEOS)

class BookmarkPrerenderBrowsertest : public BookmarkBrowsertest {
 public:
  BookmarkPrerenderBrowsertest()
      : prerender_helper_(
            base::BindRepeating(&BookmarkPrerenderBrowsertest::GetWebContents,
                                base::Unretained(this))) {}
  ~BookmarkPrerenderBrowsertest() override = default;
  BookmarkPrerenderBrowsertest(const BookmarkPrerenderBrowsertest&) = delete;

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

  void SetUp() override {
    prerender_helper_.RegisterServerRequestMonitor(embedded_test_server());
    BookmarkBrowsertest::SetUp();
  }

  void SetUpOnMainThread() override {
    host_resolver()->AddRule("*", "127.0.0.1");
    ASSERT_TRUE(embedded_test_server()->Start());
    BookmarkBrowsertest::SetUpOnMainThread();
  }

  content::test::PrerenderTestHelper& prerender_test_helper() {
    return prerender_helper_;
  }

  content::WebContents* GetWebContents() {
    return browser()->GetTabStripModel()->GetActiveWebContents();
  }

 private:
  content::test::PrerenderTestHelper prerender_helper_;
};

IN_PROC_BROWSER_TEST_F(BookmarkPrerenderBrowsertest,
                       PrerenderingShouldNotUpdateStarredState) {
  GURL initial_url = embedded_test_server()->GetURL("/empty.html");
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), initial_url));

  BookmarkModel* bookmark_model = WaitForBookmarkModel(browser()->GetProfile());
  GURL bookmark_url = embedded_test_server()->GetURL("/title1.html");
  bookmarks::AddIfNotBookmarked(bookmark_model, bookmark_url, u"Bookmark");

  BookmarkTabHelper* tab_helper =
      BookmarkTabHelper::FromWebContents(GetWebContents());
  TestBookmarkTabHelperObserver bookmark_observer(tab_helper);

  // Load a prerender page and prerendering should not notify to
  // URLStarredChanged listener.
  const content::PrerenderHostId host_id =
      prerender_test_helper().AddPrerender(bookmark_url);
  content::test::PrerenderHostObserver host_observer(*GetWebContents(),
                                                     host_id);
  EXPECT_FALSE(host_observer.was_activated());
  EXPECT_FALSE(bookmark_observer.is_starred());

  // Activate the prerender page.
  prerender_test_helper().NavigatePrimaryPage(bookmark_url);
  EXPECT_TRUE(host_observer.was_activated());
  EXPECT_TRUE(bookmark_observer.is_starred());
}
