// Copyright 2018 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/browsing_data/navigation_entry_remover.h"

#include "base/files/file_path.h"
#include "build/build_config.h"
#include "chrome/browser/sessions/tab_restore_service_factory.h"
#include "chrome/browser/ui/browser_commands.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/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/buildflags.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/sessions/core/tab_restore_service.h"
#include "content/public/browser/navigation_controller.h"
#include "content/public/browser/navigation_entry.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/test_navigation_observer.h"
#include "url/gurl.h"

#if BUILDFLAG(ENABLE_SESSION_SERVICE)
#include "chrome/browser/sessions/session_service.h"
#include "chrome/browser/sessions/session_service_factory.h"
#endif  // BUILDFLAG(ENABLE_SESSION_SERVICE)

using history::DeletionInfo;

class NavigationEntryRemoverTest : public InProcessBrowserTest {
 protected:
  void SetUpOnMainThread() override {
    auto path = base::FilePath(FILE_PATH_LITERAL("browsing_data"));
    url_a_ = chrome_test_utils::GetTestUrl(
        path, base::FilePath(FILE_PATH_LITERAL("a.html")));
    url_b_ = chrome_test_utils::GetTestUrl(
        path, base::FilePath(FILE_PATH_LITERAL("b.html")));
    url_c_ = chrome_test_utils::GetTestUrl(
        path, base::FilePath(FILE_PATH_LITERAL("c.html")));
    url_d_ = chrome_test_utils::GetTestUrl(
        path, base::FilePath(FILE_PATH_LITERAL("d.html")));
    about_blank_ = GURL("about:blank");
  }

  Profile* profile() { return browser()->GetProfile(); }

  void AddNavigations(BrowserWindowInterface* browser,
                      const std::vector<GURL>& urls) {
    for (const GURL& url : urls) {
      ui_test_utils::NavigateToURLWithDisposition(
          browser, url, WindowOpenDisposition::CURRENT_TAB,
          ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
    }
  }

  void AddTab(BrowserWindowInterface* browser, const std::vector<GURL>& urls) {
    ui_test_utils::NavigateToURLWithDisposition(
        browser, urls[0], WindowOpenDisposition::NEW_FOREGROUND_TAB,
        ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB |
            ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);
    AddNavigations(browser, {urls.begin() + 1, urls.end()});
  }

  BrowserWindowInterface* AddBrowser(BrowserWindowInterface* browser,
                                     const std::vector<GURL>& urls) {
    ui_test_utils::BrowserCreatedObserver browser_created_observer;
    ui_test_utils::NavigateToURLWithDisposition(
        browser, urls[0], WindowOpenDisposition::NEW_WINDOW,
        ui_test_utils::BROWSER_TEST_WAIT_FOR_BROWSER);
    BrowserWindowInterface* new_browser = browser_created_observer.Wait();
#if BUILDFLAG(IS_MAC)
    content::HandleMissingKeyWindow();
#endif
    ui_test_utils::WaitUntilBrowserBecomeActive(new_browser);
    AddNavigations(new_browser, {urls.begin() + 1, urls.end()});
    return new_browser;
  }

  void GoBack(content::WebContents* web_contents) {
    content::LoadStopObserver load_stop_observer(web_contents);
    web_contents->GetController().GoBack();
    load_stop_observer.Wait();
  }

  std::vector<GURL> GetEntries() {
    std::vector<GURL> urls;
    ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
        [&urls](BrowserWindowInterface* browser) {
          TabStripModel* const tab_strip_model = browser->GetTabStripModel();
          for (int j = 0; j < tab_strip_model->count(); j++) {
            content::NavigationController* const controller =
                &tab_strip_model->GetWebContentsAt(j)->GetController();
            for (int i = 0; i < controller->GetEntryCount(); i++) {
              urls.push_back(controller->GetEntryAtIndex(i)->GetURL());
            }
          }
          return true;
        });
    return urls;
  }

  DeletionInfo CreateDeletionInfo(base::Time from,
                                  base::Time to,
                                  std::set<GURL> restrict_urls = {}) {
    return DeletionInfo(history::DeletionTimeRange(from, to), false, {}, {},
                        restrict_urls.empty() ? std::optional<std::set<GURL>>()
                                              : restrict_urls);
  }

  // Helper to compare vectors. The macro gets confused by EXPECT_EQ(v, {a,b}).
  void ExpectEntries(const std::vector<GURL>& expected,
                     const std::vector<GURL>& actual) {
    EXPECT_EQ(expected, actual);
  }

  // Asserts that SessionService::DeleteLastSession has been called `count`
  // times if session service is enabled for testing.
  void ExpectDeleteLastSessionCalled(int count) {
#if BUILDFLAG(ENABLE_SESSION_SERVICE)
    SessionService* session_service =
        SessionServiceFactory::GetForProfile(profile());
    ASSERT_TRUE(session_service);
    EXPECT_EQ(count, session_service->count_delete_last_session_for_testing());
#endif  // BUILDFLAG(ENABLE_SESSION_SERVICE)
  }

  GURL url_a_;
  GURL url_b_;
  GURL url_c_;
  GURL url_d_;
  GURL about_blank_;

};

// === Tests for helper functions ===
IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, AddNavigation) {
  // A new browser starts with about:blank. Add a,b,c and check.
  ExpectEntries({about_blank_}, GetEntries());
  AddNavigations(browser(), {url_a_, url_b_, url_c_});
  ExpectEntries({about_blank_, url_a_, url_b_, url_c_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, AddTab) {
  EXPECT_EQ(1, browser()->tab_strip_model()->count());

  AddTab(browser(), {url_a_});
  EXPECT_EQ(2, browser()->tab_strip_model()->count());
  ExpectEntries({about_blank_, url_a_}, GetEntries());

  AddTab(browser(), {url_b_, url_c_});
  EXPECT_EQ(3, browser()->tab_strip_model()->count());
  ExpectEntries({about_blank_, url_a_, url_b_, url_c_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, AddWindow) {
  EXPECT_EQ(1U, GlobalBrowserCollection::GetInstance()->GetSize());

  AddBrowser(browser(), {url_a_, url_b_});
  EXPECT_EQ(2U, GlobalBrowserCollection::GetInstance()->GetSize());
  ExpectEntries({url_a_, url_b_, about_blank_}, GetEntries());

  AddBrowser(browser(), {url_c_, url_d_});
  EXPECT_EQ(3U, GlobalBrowserCollection::GetInstance()->GetSize());
  ExpectEntries({url_c_, url_d_, url_a_, url_b_, about_blank_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, GoBack) {
  AddNavigations(browser(), {url_a_, url_b_, url_c_});
  auto* web_contents = browser()->tab_strip_model()->GetActiveWebContents();
  EXPECT_EQ(url_c_, web_contents->GetLastCommittedURL());
  GoBack(web_contents);
  EXPECT_EQ(url_b_, web_contents->GetLastCommittedURL());
  ExpectEntries({about_blank_, url_a_, url_b_, url_c_}, GetEntries());
}

// === The actual tests ===
IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, DeleteIndividual) {
  AddNavigations(browser(), {url_a_, url_b_, url_c_, url_d_});
  browsing_data::RemoveNavigationEntries(
      profile(),
      DeletionInfo::ForUrls({history::URLResult(url_b_, base::Time())}, {}));
  ExpectEntries({about_blank_, url_a_, url_c_, url_d_}, GetEntries());

  browsing_data::RemoveNavigationEntries(
      profile(),
      DeletionInfo::ForUrls({history::URLResult(url_c_, base::Time())}, {}));
  ExpectEntries({about_blank_, url_a_, url_d_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, DeleteAfterNavigation) {
  AddNavigations(browser(), {url_a_, url_b_});
  browsing_data::RemoveNavigationEntries(
      profile(),
      DeletionInfo::ForUrls({history::URLResult(url_b_, base::Time())}, {}));
  // The committed entry can't be removed.
  ExpectEntries({about_blank_, url_a_, url_b_}, GetEntries());

  AddNavigations(browser(), {url_c_});
  browsing_data::RemoveNavigationEntries(
      profile(),
      DeletionInfo::ForUrls({history::URLResult(url_b_, base::Time())}, {}));
  ExpectEntries({about_blank_, url_a_, url_c_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, DeleteAll) {
  AddNavigations(browser(), {url_a_, url_b_, url_c_});
  browsing_data::RemoveNavigationEntries(profile(),
                                         DeletionInfo::ForAllHistory());
  ExpectEntries({url_c_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, DeleteRestricted) {
  AddNavigations(browser(), {url_a_, url_b_, url_c_});
  browsing_data::RemoveNavigationEntries(
      profile(), CreateDeletionInfo(base::Time(), base::Time::Now(), {url_b_}));
  ExpectEntries({about_blank_, url_a_, url_c_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, DeleteRange) {
  base::Time t1 = base::Time::Now();
  AddNavigations(browser(), {url_a_});
  base::Time t2 = base::Time::Now();
  AddNavigations(browser(), {url_b_});
  base::Time t3 = base::Time::Now();
  AddNavigations(browser(), {url_c_, url_d_});
  ASSERT_NE(t1, t2);
  ASSERT_NE(t2, t3);

  browsing_data::RemoveNavigationEntries(profile(), CreateDeletionInfo(t2, t3));
  ExpectEntries({about_blank_, url_a_, url_c_, url_d_}, GetEntries());
  browsing_data::RemoveNavigationEntries(profile(),
                                         CreateDeletionInfo(base::Time(), t1));
  ExpectEntries({url_a_, url_c_, url_d_}, GetEntries());
  browsing_data::RemoveNavigationEntries(profile(),
                                         CreateDeletionInfo(t3, base::Time()));
  ExpectEntries({url_a_, url_d_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, DeleteRangeRestricted) {
  base::Time t1 = base::Time::Now();
  AddNavigations(browser(), {url_a_});
  base::Time t2 = base::Time::Now();
  AddNavigations(browser(), {url_b_});
  base::Time t3 = base::Time::Now();
  AddNavigations(browser(), {url_c_, url_a_});
  ASSERT_NE(t1, t2);
  ASSERT_NE(t2, t3);

  browsing_data::RemoveNavigationEntries(profile(),
                                         CreateDeletionInfo(t1, t3, {url_b_}));
  ExpectEntries({about_blank_, url_a_, url_c_, url_a_}, GetEntries());
  browsing_data::RemoveNavigationEntries(
      profile(), CreateDeletionInfo(base::Time(), t2, {url_a_}));
  ExpectEntries({about_blank_, url_c_, url_a_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, DeleteAllAfterNavigation) {
  AddNavigations(browser(), {url_a_, url_b_, url_c_});
  browsing_data::RemoveNavigationEntries(profile(),
                                         DeletionInfo::ForAllHistory());
  ExpectEntries({url_c_}, GetEntries());

  AddNavigations(browser(), {url_d_});
  ExpectEntries({url_c_, url_d_}, GetEntries());

  browsing_data::RemoveNavigationEntries(profile(),
                                         DeletionInfo::ForAllHistory());
  ExpectEntries({url_d_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, TwoTabsDeletion) {
  AddNavigations(browser(), {url_a_, url_b_});
  AddTab(browser(), {url_c_, url_d_});
  browsing_data::RemoveNavigationEntries(profile(),
                                         DeletionInfo::ForAllHistory());

  ExpectEntries({url_b_, url_d_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, TwoWindowsDeletion) {
  AddNavigations(browser(), {url_a_, url_b_});
  AddBrowser(browser(), {url_c_, url_d_});

  browsing_data::RemoveNavigationEntries(profile(),
                                         DeletionInfo::ForAllHistory());

  ExpectEntries({url_d_, url_b_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, GoBackAndDelete) {
  AddNavigations(browser(), {url_a_, url_b_, url_c_});

  GoBack(browser()->tab_strip_model()->GetActiveWebContents());
  browsing_data::RemoveNavigationEntries(profile(),
                                         DeletionInfo::ForAllHistory());

  ExpectEntries({url_b_}, GetEntries());
}

IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest, RecentTabDeletion) {
  AddNavigations(browser(), {url_a_, url_b_});
  AddTab(browser(), {url_c_});
  AddTab(browser(), {url_d_});
  chrome::CloseTab(browser());
  chrome::CloseTab(browser());

  sessions::TabRestoreService* tab_service =
      TabRestoreServiceFactory::GetForProfile(profile());
  EXPECT_EQ(2U, tab_service->entries().size());
  ExpectDeleteLastSessionCalled(0);

  browsing_data::RemoveNavigationEntries(
      profile(),
      DeletionInfo::ForUrls({history::URLResult(url_c_, base::Time())}, {}));
  content::RunAllTasksUntilIdle();
  EXPECT_EQ(1U, tab_service->entries().size());
  auto* tab = static_cast<sessions::tab_restore::Tab*>(
      tab_service->entries().front().get());
  EXPECT_EQ(url_d_, tab->navigations.front().virtual_url());
  EXPECT_TRUE(tab_service->IsLoaded());
  ExpectDeleteLastSessionCalled(1);

  browsing_data::RemoveNavigationEntries(
      profile(),
      DeletionInfo::ForUrls({history::URLResult(url_d_, base::Time())}, {}));
  EXPECT_EQ(0U, tab_service->entries().size());
  ExpectDeleteLastSessionCalled(2);
}

// TODO(crbug.com/40283363): flaky on windows.
#if BUILDFLAG(IS_WIN)
#define MAYBE_RecentTabWindowDeletion DISABLED_RecentTabWindowDeletion
#else
#define MAYBE_RecentTabWindowDeletion RecentTabWindowDeletion
#endif
IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest,
                       MAYBE_RecentTabWindowDeletion) {
  // Create a new browser with three tabs and close it.
  BrowserWindowInterface* new_browser = AddBrowser(browser(), {url_a_});
  AddTab(new_browser, {url_b_, url_c_});
  AddTab(new_browser, {url_d_});
  chrome::CloseWindow(new_browser);

  sessions::TabRestoreService* tab_service =
      TabRestoreServiceFactory::GetForProfile(profile());
  EXPECT_EQ(1U, tab_service->entries().size());
  ASSERT_EQ(sessions::tab_restore::Type::WINDOW,
            tab_service->entries().front()->type);
  auto* window = static_cast<sessions::tab_restore::Window*>(
      tab_service->entries().front().get());
  EXPECT_EQ(3U, window->tabs.size());
  ExpectDeleteLastSessionCalled(0);

  // Delete b and d. The last opened tab should be removed.
  browsing_data::RemoveNavigationEntries(
      profile(),
      DeletionInfo::ForUrls({history::URLResult(url_b_, base::Time()),
                             history::URLResult(url_d_, base::Time())},
                            {}));
  content::RunAllTasksUntilIdle();
  EXPECT_EQ(1U, tab_service->entries().size());
  ASSERT_EQ(sessions::tab_restore::Type::WINDOW,
            tab_service->entries().front()->type);
  window = static_cast<sessions::tab_restore::Window*>(
      tab_service->entries().front().get());
  EXPECT_EQ(2U, window->tabs.size());
  EXPECT_EQ(2U, window->tabs.size());
  EXPECT_EQ(1, window->selected_tab_index);
  EXPECT_EQ(0, window->tabs[0]->tabstrip_index);
  EXPECT_EQ(1, window->tabs[1]->tabstrip_index);
  EXPECT_EQ(url_a_, window->tabs[0]->navigations.front().virtual_url());
  EXPECT_EQ(url_c_, window->tabs[1]->navigations.front().virtual_url());
  ExpectDeleteLastSessionCalled(1);

  // Delete a. The Window should be converted to a Tab.
  browsing_data::RemoveNavigationEntries(
      profile(),
      DeletionInfo::ForUrls({history::URLResult(url_a_, base::Time())}, {}));
  EXPECT_EQ(1U, tab_service->entries().size());
  ASSERT_EQ(sessions::tab_restore::Type::TAB,
            tab_service->entries().front()->type);
  auto* tab = static_cast<sessions::tab_restore::Tab*>(
      tab_service->entries().front().get());
  EXPECT_EQ(url_c_, tab->navigations.front().virtual_url());
  EXPECT_EQ(0, tab->tabstrip_index);
  ExpectDeleteLastSessionCalled(2);
}

// Checks that we do not attempt to delete SessionService data when processing a
// foreign history delete.
// Test for crbug.com/40063610.
IN_PROC_BROWSER_TEST_F(NavigationEntryRemoverTest,
                       ForeignHistoryDeleteDoesNotDeleteSessionServiceData) {
  AddNavigations(browser(), {url_a_, url_b_, url_c_, url_d_});
  const DeletionInfo info =
      DeletionInfo(history::DeletionTimeRange(base::Time(), base::Time::Now()),
                   /*is_from_expiration=*/false,
                   DeletionInfo::Reason::kDeleteAllForeignVisits,
                   /*deleted_rows=*/{}, /*deleted_visit_ids=*/{},
                   /*favicon_urls=*/{}, /*restrict_urls=*/{{url_b_}});
  ExpectDeleteLastSessionCalled(0);

  // Tab restore data is should be selectively deleted but there should not be
  // a request to delete session service data.
  browsing_data::RemoveNavigationEntries(profile(), info);
  ExpectEntries({about_blank_, url_a_, url_c_, url_d_}, GetEntries());
  ExpectDeleteLastSessionCalled(0);
}
