// Copyright 2021 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/breadcrumbs/breadcrumb_manager_tab_helper.h"

#include <memory>

#include "base/containers/circular_deque.h"
#include "base/format_macros.h"
#include "base/strings/stringprintf.h"
#include "build/build_config.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/breadcrumbs/core/breadcrumb_manager.h"
#include "components/breadcrumbs/core/breadcrumb_manager_tab_helper.h"
#include "components/infobars/content/content_infobar_manager.h"
#include "components/infobars/core/infobar.h"
#include "components/infobars/core/infobar_delegate.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/test/navigation_simulator.h"
#include "content/public/test/test_web_contents_factory.h"
#include "content/test/test_web_contents.h"
#include "testing/gtest/include/gtest/gtest.h"

using infobars::InfoBarDelegate;

namespace {

class FakeInfoBarDelegate : public infobars::InfoBarDelegate {
 public:
  explicit FakeInfoBarDelegate(
      infobars::InfoBarDelegate::InfoBarIdentifier identifier)
      : identifier_(identifier) {}

  ~FakeInfoBarDelegate() override = default;

  infobars::InfoBarDelegate::InfoBarIdentifier GetIdentifier() const override {
    return identifier_;
  }

  bool EqualsDelegate(infobars::InfoBarDelegate* delegate) const override {
    return delegate->GetIdentifier() == GetIdentifier();
  }

  infobars::InfoBarDelegate::InfoBarIdentifier identifier_;
};

std::unique_ptr<infobars::InfoBar> CreateInfoBar(
    infobars::InfoBarDelegate::InfoBarIdentifier identifier) {
  auto infobar_delegate = std::make_unique<FakeInfoBarDelegate>(identifier);
  return std::make_unique<infobars::InfoBar>(std::move(infobar_delegate));
}

const base::circular_deque<std::string>& GetEvents() {
  return breadcrumbs::BreadcrumbManager::GetInstance().GetEvents();
}

size_t GetNumEvents() {
  return GetEvents().size();
}

}  // namespace

// Test fixture for BreadcrumbManagerTabHelper class.
class BreadcrumbManagerTabHelperTest : public ChromeRenderViewHostTestHarness {
 protected:
  void SetUp() override {
    ChromeRenderViewHostTestHarness::SetUp();
    second_web_contents_ = CreateTestWebContents();
    infobars::ContentInfoBarManager::CreateForWebContents(web_contents());
    infobars::ContentInfoBarManager::CreateForWebContents(
        second_web_contents_.get());
    BreadcrumbManagerTabHelper::CreateForWebContents(web_contents());
    BreadcrumbManagerTabHelper::CreateForWebContents(
        second_web_contents_.get());
  }

  void TearDown() override {
    second_web_contents_.reset();
    ChromeRenderViewHostTestHarness::TearDown();
  }

  std::unique_ptr<content::WebContents> second_web_contents_;

  const GURL kTestURL = GURL("https://test");
};

// Tests that the identifiers returned for different WebContents are unique.
TEST_F(BreadcrumbManagerTabHelperTest, UniqueIdentifiers) {
  const int first_tab_identifier =
      BreadcrumbManagerTabHelper::FromWebContents(web_contents())
          ->GetUniqueId();
  const int second_tab_identifier =
      BreadcrumbManagerTabHelper::FromWebContents(second_web_contents_.get())
          ->GetUniqueId();

  EXPECT_GT(first_tab_identifier, 0);
  EXPECT_GT(second_tab_identifier, 0);
  EXPECT_NE(first_tab_identifier, second_tab_identifier);
}

// Tests that BreadcrumbManagerTabHelper events are logged to the
// associated BreadcrumbManagerKeyedService.
TEST_F(BreadcrumbManagerTabHelperTest, EventsLogged) {
  ASSERT_EQ(0u, GetNumEvents());

  auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
      kTestURL, web_contents());
  simulator->Start();

  auto events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.back();

  simulator->Commit();
  events = GetEvents();
  ASSERT_EQ(3u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbPageLoaded))
      << events.back();
  events.pop_back();
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbDidFinishNavigation))
      << events.back();
}

// Tests that BreadcrumbManagerTabHelper events logged from separate
// WebContents are unique.
TEST_F(BreadcrumbManagerTabHelperTest, UniqueEvents) {
  auto first_simulator = content::NavigationSimulator::CreateBrowserInitiated(
      kTestURL, web_contents());
  first_simulator->Start();
  auto second_simulator = content::NavigationSimulator::CreateBrowserInitiated(
      kTestURL, second_web_contents_.get());
  second_simulator->Start();
  const auto& events = GetEvents();
  ASSERT_EQ(2u, events.size());
  EXPECT_STRNE(events.front().c_str(), events.back().c_str());
  EXPECT_NE(std::string::npos,
            events.front().find(breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.front();
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.back();
}

// Tests metadata for www.google.com navigation.
TEST_F(BreadcrumbManagerTabHelperTest, GoogleNavigationStart) {
  ASSERT_EQ(0u, GetNumEvents());
  auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
      GURL("https://www.google.com"), web_contents());
  simulator->Start();
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_NE(std::string::npos,
            events.front().find(breadcrumbs::kBreadcrumbGoogleNavigation))
      << events.front();
}

// Tests metadata for https://play.google.com/ navigation.
TEST_F(BreadcrumbManagerTabHelperTest, GooglePlayNavigationStart) {
  ASSERT_EQ(0u, GetNumEvents());
  auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
      GURL("https://play.google.com/"), web_contents());
  simulator->Start();
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  // #google is useful to indicate SRP. There is no need to know URLs of other
  // visited google properties.
  EXPECT_EQ(std::string::npos,
            events.front().find(breadcrumbs::kBreadcrumbGoogleNavigation))
      << events.front();
}

// TODO(crbug.com/40740494): special handling is needed for new-tab-page tests
// on Android, as it uses a different new-tab URL.
#if !BUILDFLAG(IS_ANDROID)
// Tests metadata for chrome://newtab NTP navigation.
TEST_F(BreadcrumbManagerTabHelperTest, ChromeNewTabNavigationStart) {
  ASSERT_EQ(0u, GetNumEvents());
  auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
      chrome::ChromeUINewTabURLAsGURL(), web_contents());
  simulator->Start();
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_NE(std::string::npos,
            events.front().find(base::StringPrintf(
                "%s%" PRIu64, breadcrumbs::kBreadcrumbDidStartNavigation,
                simulator->GetNavigationHandle()->GetNavigationId())))
      << events.front();
  EXPECT_NE(std::string::npos,
            events.front().find(breadcrumbs::kBreadcrumbNtpNavigation))
      << events.front();
}
#endif  // !BUILDFLAG(IS_ANDROID)

// Tests unique ID in DidStartNavigation and DidFinishNavigation.
TEST_F(BreadcrumbManagerTabHelperTest, NavigationUniqueId) {
  ASSERT_EQ(0u, GetNumEvents());
  // DidStartNavigation
  auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
      kTestURL, web_contents());
  simulator->Start();
  auto events = GetEvents();
  ASSERT_EQ(1u, events.size());
  const int64_t navigation_id =
      simulator->GetNavigationHandle()->GetNavigationId();
  EXPECT_NE(std::string::npos,
            events.front().find(base::StringPrintf(
                "%s%" PRIu64, breadcrumbs::kBreadcrumbDidStartNavigation,
                navigation_id)))
      << events.front();
  // DidFinishNavigation
  simulator->Commit();
  events = GetEvents();
  ASSERT_EQ(3u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbPageLoaded))
      << events.back();
  events.pop_back();
  EXPECT_NE(std::string::npos,
            events.back().find(base::StringPrintf(
                "%s%" PRIu64, breadcrumbs::kBreadcrumbDidFinishNavigation,
                navigation_id)))
      << events.back();
}

// Tests renderer initiated metadata in DidStartNavigation.
TEST_F(BreadcrumbManagerTabHelperTest, RendererInitiatedByUser) {
  ASSERT_EQ(0u, GetNumEvents());
  auto simulator = content::NavigationSimulator::CreateRendererInitiated(
      kTestURL, web_contents()->GetPrimaryMainFrame());
  simulator->SetHasUserGesture(true);
  simulator->SetTransition(ui::PAGE_TRANSITION_LINK);
  simulator->Start();
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_NE(std::string::npos, events.back().find("#link")) << events.back();
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.back();
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbRendererInitiatedByUser))
      << events.back();
  EXPECT_EQ(
      std::string::npos,
      events.back().find(breadcrumbs::kBreadcrumbRendererInitiatedByScript))
      << events.back();
}

// Tests renderer initiated metadata in DidStartNavigation.
TEST_F(BreadcrumbManagerTabHelperTest, RendererInitiatedByScript) {
  ASSERT_EQ(0u, GetNumEvents());
  auto simulator = content::NavigationSimulator::CreateRendererInitiated(
      kTestURL, web_contents()->GetPrimaryMainFrame());
  simulator->SetHasUserGesture(false);
  simulator->Start();
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_NE(std::string::npos, events.back().find("#link")) << events.back();
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.back();
  EXPECT_EQ(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbRendererInitiatedByUser))
      << events.back();
  EXPECT_NE(
      std::string::npos,
      events.back().find(breadcrumbs::kBreadcrumbRendererInitiatedByScript))
      << events.back();
}

// Tests browser initiated metadata in DidStartNavigation.
TEST_F(BreadcrumbManagerTabHelperTest, BrowserInitiatedByScript) {
  ASSERT_EQ(0u, GetNumEvents());
  auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
      kTestURL, web_contents());
  simulator->SetTransition(ui::PAGE_TRANSITION_TYPED);
  simulator->Start();
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_NE(std::string::npos, events.back().find("#typed")) << events.back();
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbDidStartNavigation))
      << events.back();
  EXPECT_EQ(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbRendererInitiatedByUser))
      << events.back();
  EXPECT_EQ(
      std::string::npos,
      events.back().find(breadcrumbs::kBreadcrumbRendererInitiatedByScript))
      << events.back();
}

// Tests PDF load.
TEST_F(BreadcrumbManagerTabHelperTest, PdfLoad) {
  ASSERT_EQ(0u, GetNumEvents());
  auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
      kTestURL, web_contents());
  simulator->SetContentsMimeType("application/pdf");
  simulator->Commit();
  const auto& events = GetEvents();
  ASSERT_EQ(3u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbPageLoaded))
      << events.back();
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbPdfLoad))
      << events.back();
}

// Tests page load succeess.
TEST_F(BreadcrumbManagerTabHelperTest, PageLoadSuccess) {
  ASSERT_EQ(0u, GetNumEvents());
  content::NavigationSimulator::NavigateAndCommitFromBrowser(web_contents(),
                                                             kTestURL);
  const auto& events = GetEvents();
  ASSERT_EQ(3u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbPageLoaded))
      << events.back();
  EXPECT_EQ(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbPageLoadFailure))
      << events.back();
}

// Tests page load failure.
TEST_F(BreadcrumbManagerTabHelperTest, PageLoadFailure) {
  auto simulator = content::NavigationSimulator::CreateBrowserInitiated(
      kTestURL, web_contents());
  simulator->Start();
  ASSERT_EQ(1u, GetNumEvents());

  static_cast<content::TestWebContents*>(web_contents())
      ->GetPrimaryMainFrame()
      ->DidFailLoadWithError(kTestURL, net::ERR_ABORTED);
  const auto& events = GetEvents();
  ASSERT_EQ(2u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbPageLoaded))
      << events.back();
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbPageLoadFailure))
      << events.back();
}

// TODO(crbug.com/40740494): special handling is needed for new-tab-page tests
// on Android, as it uses a different new-tab URL. Tests NTP page load.
#if !BUILDFLAG(IS_ANDROID)
TEST_F(BreadcrumbManagerTabHelperTest, NtpPageLoad) {
  ASSERT_EQ(0u, GetNumEvents());
  content::NavigationSimulator::NavigateAndCommitFromBrowser(
      web_contents(), chrome::ChromeUINewTabURLAsGURL());
  const auto& events = GetEvents();
  ASSERT_EQ(3u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbPageLoaded))
      << events.back();
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbNtpNavigation))
      << events.back();
  // NTP navigation can't fail, so there is no success/failure metadata.
  EXPECT_EQ(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbPageLoadFailure))
      << events.back();
}
#endif  // !BUILDFLAG(IS_ANDROID)

// Tests navigation error.
TEST_F(BreadcrumbManagerTabHelperTest, NavigationError) {
  ASSERT_EQ(0u, GetNumEvents());
  content::NavigationSimulator::NavigateAndFailFromBrowser(
      web_contents(), kTestURL, net::ERR_INTERNET_DISCONNECTED);
  const auto& events = GetEvents();
  ASSERT_EQ(2u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbDidFinishNavigation))
      << events.back();
  EXPECT_NE(std::string::npos, events.back().find(net::ErrorToShortString(
                                   net::ERR_INTERNET_DISCONNECTED)))
      << events.back();
}

// Tests that adding an infobar logs the expected breadcrumb.
TEST_F(BreadcrumbManagerTabHelperTest, AddInfobar) {
  ASSERT_EQ(0u, GetNumEvents());
  const auto identifier = InfoBarDelegate::InfoBarIdentifier::TEST_INFOBAR;
  infobars::ContentInfoBarManager::FromWebContents(web_contents())
      ->AddInfoBar(CreateInfoBar(identifier));
  const auto& events = GetEvents();
  ASSERT_EQ(1u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(base::StringPrintf(
                "%s%d", breadcrumbs::kBreadcrumbInfobarAdded, identifier)))
      << events.back();
}

// Tests that infobar breadcrumbs specify the infobar type.
TEST_F(BreadcrumbManagerTabHelperTest, InfobarTypes) {
  ASSERT_EQ(0u, GetNumEvents());
  // Add and remove first infobar.
  const auto first_identifier =
      InfoBarDelegate::InfoBarIdentifier::DEV_TOOLS_INFOBAR_DELEGATE;
  infobars::ContentInfoBarManager::FromWebContents(web_contents())
      ->AddInfoBar(CreateInfoBar(first_identifier));
  infobars::ContentInfoBarManager::FromWebContents(web_contents())
      ->RemoveAllInfoBars(/*animate=*/false);
  // Add second infobar.
  const auto second_identifier =
      InfoBarDelegate::InfoBarIdentifier::EXTENSION_DEV_TOOLS_INFOBAR_DELEGATE;
  infobars::ContentInfoBarManager::FromWebContents(web_contents())
      ->AddInfoBar(CreateInfoBar(second_identifier));
  const auto& events = GetEvents();
  ASSERT_EQ(3u, events.size());
  EXPECT_NE(events.front(), events.back());
  EXPECT_NE(std::string::npos, events.front().find(base::StringPrintf(
                                   "%s%d", breadcrumbs::kBreadcrumbInfobarAdded,
                                   first_identifier)))
      << events.back();
  EXPECT_NE(std::string::npos, events.back().find(base::StringPrintf(
                                   "%s%d", breadcrumbs::kBreadcrumbInfobarAdded,
                                   second_identifier)))
      << events.back();
}

// Tests that removing an infobar without animation logs the expected breadcrumb
// event.
TEST_F(BreadcrumbManagerTabHelperTest, RemoveInfobarNotAnimated) {
  ASSERT_EQ(0u, GetNumEvents());
  const auto identifier = InfoBarDelegate::InfoBarIdentifier::TEST_INFOBAR;
  infobars::ContentInfoBarManager::FromWebContents(web_contents())
      ->AddInfoBar(CreateInfoBar(identifier));
  infobars::ContentInfoBarManager::FromWebContents(web_contents())
      ->RemoveAllInfoBars(/*animate=*/false);
  const auto& events = GetEvents();
  ASSERT_EQ(2u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(base::StringPrintf(
                "%s%d", breadcrumbs::kBreadcrumbInfobarRemoved, identifier)))
      << events.back();
  EXPECT_NE(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbInfobarNotAnimated))
      << events.back();
}

// Tests that removing an infobar with animation logs the expected breadcrumb
// event.
TEST_F(BreadcrumbManagerTabHelperTest, RemoveInfobarAnimated) {
  ASSERT_EQ(0u, GetNumEvents());
  const auto identifier = InfoBarDelegate::InfoBarIdentifier::TEST_INFOBAR;
  infobars::ContentInfoBarManager::FromWebContents(web_contents())
      ->AddInfoBar(CreateInfoBar(identifier));
  infobars::ContentInfoBarManager::FromWebContents(web_contents())
      ->RemoveAllInfoBars(/*animate=*/true);
  const auto& events = GetEvents();
  ASSERT_EQ(2u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(base::StringPrintf(
                "%s%d", breadcrumbs::kBreadcrumbInfobarRemoved, identifier)))
      << events.back();
  EXPECT_EQ(std::string::npos,
            events.back().find(breadcrumbs::kBreadcrumbInfobarNotAnimated))
      << events.back();
}

// Tests that replacing an infobar logs the expected breadcrumb event.
TEST_F(BreadcrumbManagerTabHelperTest, ReplaceInfobar) {
  ASSERT_EQ(0u, GetNumEvents());
  const auto identifier = InfoBarDelegate::InfoBarIdentifier::TEST_INFOBAR;
  infobars::ContentInfoBarManager::FromWebContents(web_contents())
      ->AddInfoBar(CreateInfoBar(identifier));
  infobars::ContentInfoBarManager::FromWebContents(web_contents())
      ->AddInfoBar(CreateInfoBar(identifier),
                   /*replace_existing=*/true);
  const auto& events = GetEvents();
  ASSERT_EQ(2u, events.size());
  EXPECT_NE(std::string::npos,
            events.back().find(base::StringPrintf(
                "%s%d", breadcrumbs::kBreadcrumbInfobarReplaced, identifier)))
      << events.back();
}

// Tests that replacing an infobar many times only logs the replaced infobar
// breadcrumb at major increments.
TEST_F(BreadcrumbManagerTabHelperTest, SequentialInfobarReplacements) {
  ASSERT_EQ(0u, GetNumEvents());
  const auto identifier = InfoBarDelegate::InfoBarIdentifier::TEST_INFOBAR;
  infobars::ContentInfoBarManager::FromWebContents(web_contents())
      ->AddInfoBar(CreateInfoBar(identifier));
  for (int replacements = 0; replacements < 500; replacements++) {
    infobars::ContentInfoBarManager::FromWebContents(web_contents())
        ->AddInfoBar(CreateInfoBar(identifier),
                     /*replace_existing=*/true);
  }
  const auto& events = GetEvents();
  // Replacing the infobar 500 times should only log breadcrumbs on the 1st,
  // 2nd, 5th, 20th, 100th, 200th replacement.
  ASSERT_EQ(7u, events.size());
  // The events should contain the number of times the info has been replaced.
  // Validate the last one, which occurs at the 200th replacement.
  const std::string expected_event = base::StringPrintf(
      "%s%d %d", breadcrumbs::kBreadcrumbInfobarReplaced, identifier, 200);
  EXPECT_NE(std::string::npos, events.back().find(expected_event))
      << events.back();
}
