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

#include <optional>

#include "base/feature_list.h"
#include "base/metrics/histogram_base.h"
#include "base/metrics/statistics_recorder.h"
#include "base/run_loop.h"
#include "base/test/bind.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/scoped_run_loop_timeout.h"
#include "base/test/test_timeouts.h"
#include "base/test/with_feature_override.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/signin/e2e_tests/account_capabilities_observer.h"
#include "chrome/browser/signin/e2e_tests/accounts_removed_waiter.h"
#include "chrome/browser/signin/e2e_tests/live_test.h"
#include "chrome/browser/signin/e2e_tests/sign_in_test_observer.h"
#include "chrome/browser/signin/e2e_tests/signin_util.h"
#include "chrome/browser/signin/identity_manager_factory.h"
#include "chrome/browser/ui/browser.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/lens/lens_overlay_controller.h"
#include "chrome/browser/ui/lens/lens_overlay_side_panel_coordinator.h"
#include "chrome/browser/ui/lens/lens_search_controller.h"
#include "chrome/browser/ui/side_panel/side_panel_enums.h"
#include "chrome/browser/ui/side_panel/side_panel_ui.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/lens/lens_features.h"
#include "components/lens/lens_overlay_invocation_source.h"
#include "components/lens/lens_overlay_permission_utils.h"
#include "components/prefs/pref_service.h"
#include "components/signin/core/browser/account_reconcilor.h"
#include "components/signin/public/identity_manager/identity_manager.h"
#include "components/signin/public/identity_manager/identity_test_utils.h"
#include "components/signin/public/identity_manager/test_accounts.h"
#include "components/sync/base/features.h"
#include "components/sync/base/user_selectable_type.h"
#include "components/sync/service/sync_service.h"
#include "components/sync/service/sync_user_settings.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 "net/dns/mock_host_resolver.h"
#include "ui/compositor/compositor_switches.h"
#include "ui/gfx/scoped_animation_duration_scale_mode.h"

namespace lens {

namespace {
using State = LensOverlayController::State;
using LensOverlayInvocationSource = lens::LensOverlayInvocationSource;

constexpr char kResultsSearchBaseUrl[] = "https://www.google.com/search";

constexpr char kDivObjectClass[] = "object";
// Helper script to verify that the overlay WebUI has rendered divs with the CSS
// class provided.
constexpr char kFindAndClickDivWithClassScript[] = R"(
      function findAndClickDivWithClass(parentElement) {
        const div = parentElement.querySelector('div.' + $1);
        if (div) {
            const rect = div.getBoundingClientRect();
            const centerX = rect.left + rect.width / 2;
            const centerY = rect.top + rect.height / 2;

            div.dispatchEvent(new PointerEvent('pointerdown', {
              pointerId: 1,
              button: 0,
              clientX: centerX,
              clientY: centerY,
              isPrimary: true,
              bubbles: true,
              composed: true
            }));
            div.dispatchEvent(new PointerEvent('pointerup', {
              pointerId: 1,
              button: 0,
              clientX: centerX,
              clientY: centerY,
              isPrimary: true,
              bubbles: true,
              composed: true
            }));
            return true;
        }
        for (const child of parentElement.children) {
            if (findAndClickDivWithClass(child) ||
                (child.shadowRoot &&
                    findAndClickDivWithClass(child.shadowRoot))) {
                return true;
            }
        }
        return false;
      }
      findAndClickDivWithClass(document.body);
)";

const char kNpsObjectUrl[] =
    "https://www.nps.gov/common/commonspot/templates/images/graphics/404/"
    "04.jpg";
}  // namespace

// Live tests for Lens Overlay.
// These tests can be run with:
// browser_tests --gtest_filter=LensOverlayLiveTest.* --run-live-tests
class LensOverlayLiveTest : public base::test::WithFeatureOverride,
                            public signin::test::LiveTest {
 public:
  LensOverlayLiveTest()
      : base::test::WithFeatureOverride(
            syncer::kReplaceSyncPromosWithSignInPromos) {}
  ~LensOverlayLiveTest() override = default;

  void SetUp() override {
    SetUpFeatureList();
    LiveTest::SetUp();
    // Always disable animation for stability.
    gfx::ScopedAnimationDurationScaleMode disable_animation(
        gfx::ScopedAnimationDurationScaleMode::ZERO_DURATION);
  }

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

    // Permits sharing the page screenshot by default. This disables the
    // permission dialog.
    PrefService* prefs = browser()->GetProfile()->GetPrefs();
    prefs->SetBoolean(lens::prefs::kLensSharingPageScreenshotEnabled, true);

    // Set the default timeout for our run loops.
    base::test::ScopedRunLoopTimeout timeout(FROM_HERE,
                                             TestTimeouts::action_timeout());
  }

  void SetUpInProcessBrowserTestFixture() override {
    // Allowlists hosts.
    host_resolver()->AllowDirectLookup("*.nps.gov");

    LiveTest::SetUpInProcessBrowserTestFixture();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    LiveTest::SetUpCommandLine(command_line);
    // Because we are taking a screenshot of a live page, we need to enable
    // pixel output in tests.
    command_line->AppendSwitch(::switches::kEnablePixelOutputInTests);
  }

  syncer::SyncService* sync_service() {
    return signin::test::sync_service(browser());
  }

  bool IsLensOverlaySidePanelShowing() {
    return browser()->GetFeatures().side_panel_ui()->IsSidePanelEntryShowing(
        SidePanelEntryKey(SidePanelEntryId::kLensOverlayResults));
  }

  signin::test::SignInFunctions sign_in_functions =
      signin::test::SignInFunctions(
          base::BindLambdaForTesting(
              [this]() -> BrowserWindowInterface* { return this->browser(); }),
          base::BindLambdaForTesting(
              [this](int index,
                     const GURL& url,
                     ui::PageTransition transition) -> bool {
                return this->AddTabAtIndex(index, url, transition);
              }));

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

  content::WebContents* GetOverlayWebContents() {
    auto* controller = browser()
                           ->GetTabStripModel()
                           ->GetActiveTab()
                           ->GetTabFeatures()
                           ->lens_overlay_controller();
    return controller->GetOverlayWebViewForTesting()->GetWebContents();
  }

  content::EvalJsResult EvalJs(const std::string& code) {
    // Execute JS in Overlay WebUI.
    return content::EvalJs(GetOverlayWebContents()->GetPrimaryMainFrame(),
                           code);
  }

  void WaitForHistogram(const std::string& histogram_name) {
    // Continue if histogram was already recorded.
    if (base::StatisticsRecorder::FindHistogram(histogram_name)) {
      return;
    }

    // Else, wait until the histogram is recorded.
    base::RunLoop run_loop;
    auto histogram_observer = std::make_unique<
        base::StatisticsRecorder::ScopedHistogramSampleObserver>(
        histogram_name,
        base::BindLambdaForTesting(
            [&](std::string_view histogram_name, uint64_t name_hash,
                base::HistogramBase::Sample32 sample) { run_loop.Quit(); }));
    run_loop.Run();
  }

  // Lens overlay takes a screenshot of the tab. In order to take a screenshot
  // the tab must not be about:blank and must be painted. By default opens in
  // the current tab.
  void WaitForPaint(
      std::string_view url,
      WindowOpenDisposition disposition = WindowOpenDisposition::CURRENT_TAB,
      int browser_test_flags = ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP) {
    ASSERT_TRUE(ui_test_utils::NavigateToURLWithDisposition(
        browser(), GURL(url), disposition, browser_test_flags));
    ASSERT_TRUE(base::test::RunUntil([&]() {
      return browser()
          ->GetTabStripModel()
          ->GetActiveTab()
          ->GetContents()
          ->CompletedFirstVisuallyNonEmptyPaint();
    }));
  }

  // Verifies the side panel opened and loaded a search URL in its iframe.
  void VerifySidePanelLoaded() {
    auto* controller = browser()
                           ->GetTabStripModel()
                           ->GetActiveTab()
                           ->GetTabFeatures()
                           ->lens_overlay_controller();

    // Expect the Lens Overlay results panel to open.
    ASSERT_TRUE(base::test::RunUntil(
        [&]() { return IsLensOverlaySidePanelShowing(); }));

    // Wait for the panel to finish loading.
    EXPECT_TRUE(content::WaitForLoadStop(
        controller->GetSidePanelWebContentsForTesting()));

    // The results frame should be the only child frame of the side panel web
    // contents.
    content::RenderFrameHost* results_frame = content::ChildFrameAt(
        controller->GetSidePanelWebContentsForTesting()->GetPrimaryMainFrame(),
        0);
    EXPECT_TRUE(results_frame);
    EXPECT_TRUE(content::WaitForRenderFrameReady(results_frame));

    // Check the result frame URL matches a valid results URL.
    EXPECT_THAT(results_frame->GetLastCommittedURL().spec(),
                testing::MatchesRegex(
                    std::string(kResultsSearchBaseUrl) +
                    ".*source=chrome.cr.menu.*&gsc=2&hl=.*&biw=\\d+&bih=\\d+"));
  }

  virtual void SetUpFeatureList() {
    feature_list_.InitAndEnableFeatureWithParameters(
        lens::features::kLensOverlay,
        {{"enable-shimmer", "false"}, {"use-blur", "false"}});
  }

  void TearDown() override { LiveTest::TearDown(); }

 protected:
  base::test::ScopedFeatureList feature_list_;
};

IN_PROC_BROWSER_TEST_P(LensOverlayLiveTest,
                       ClickObject_SignedInAndSyncingHistory) {
  std::optional<signin::TestAccountSigninCredentials> test_account =
      GetTestAccounts()->GetAccount("INTELLIGENCE_ACCOUNT");
  // Sign in and sync to opted in test account.
  CHECK(test_account.has_value());
  sign_in_functions.SignInFromSettingsWithSyncChoice(
      *test_account, 0,
      signin::test::SignInFunctions::SyncChoice::
          kAcceptAllOptionalDataTypesSync);

  // Navigate to a website and wait for paint before starting controller.
  WaitForPaint(kNpsObjectUrl);
  EXPECT_TRUE(content::WaitForLoadStop(web_contents()));

  // State should start in off.
  auto* controller = browser()
                         ->GetTabStripModel()
                         ->GetActiveTab()
                         ->GetTabFeatures()
                         ->lens_overlay_controller();
  ASSERT_EQ(controller->state(), State::kOff);

  auto* search_controller =
      LensSearchController::From(browser()->GetActiveTabInterface());

  // Showing UI should change the state to screenshot and eventually to overlay.
  search_controller->OpenLensOverlay(LensOverlayInvocationSource::kAppMenu);
  ASSERT_EQ(controller->state(), State::kScreenshot);
  ASSERT_TRUE(base::test::RunUntil(
      [&]() { return controller->state() == State::kOverlay; }));
  ASSERT_FALSE(IsLensOverlaySidePanelShowing());
  ASSERT_TRUE(content::WaitForLoadStop(GetOverlayWebContents()));

  // Confirm that the WebUI has reported that it is ready. This means the local
  // DOM should be initialized on our WebUI.
  WaitForHistogram("Lens.Overlay.TimeToWebUIReady");

  // Verify that the page returns objects that is selectable on the overlay.
  ASSERT_TRUE(base::test::RunUntil([&]() {
    return EvalJs(content::JsReplace(kFindAndClickDivWithClassScript,
                                     kDivObjectClass))
        .ExtractBool();
  }));

  // After finding and clicking the div, make sure the side panel opens and
  // loaded a result.
  VerifySidePanelLoaded();
}

IN_PROC_BROWSER_TEST_P(LensOverlayLiveTest, ClickObject_SignedInOnWebOnly) {
  std::optional<signin::TestAccountSigninCredentials> test_account =
      GetTestAccounts()->GetAccount("INTELLIGENCE_ACCOUNT");
  // Sign in but do not sync to opted in test account.
  CHECK(test_account.has_value());
  sign_in_functions.SignInFromWeb(*test_account, 0);
  EXPECT_FALSE(sync_service()->GetUserSettings()->GetSelectedTypes().Has(
      syncer::UserSelectableType::kHistory));

  // Navigate to a website and wait for paint before starting controller.
  WaitForPaint(kNpsObjectUrl);
  EXPECT_TRUE(content::WaitForLoadStop(web_contents()));

  // State should start in off.
  auto* controller = browser()
                         ->GetTabStripModel()
                         ->GetActiveTab()
                         ->GetTabFeatures()
                         ->lens_overlay_controller();
  ASSERT_EQ(controller->state(), State::kOff);

  auto* search_controller =
      LensSearchController::From(browser()->GetActiveTabInterface());

  // Showing UI should change the state to screenshot and eventually to overlay.
  search_controller->OpenLensOverlay(LensOverlayInvocationSource::kAppMenu);
  ASSERT_EQ(controller->state(), State::kScreenshot);
  ASSERT_TRUE(base::test::RunUntil(
      [&]() { return controller->state() == State::kOverlay; }));
  ASSERT_FALSE(IsLensOverlaySidePanelShowing());
  ASSERT_TRUE(content::WaitForLoadStop(GetOverlayWebContents()));

  // Confirm that the WebUI has reported that it is ready. This means the local
  // DOM should be initialized on our WebUI.
  WaitForHistogram("Lens.Overlay.TimeToWebUIReady");

  // Verify that the page returns objects that is selectable on the overlay.
  ASSERT_TRUE(base::test::RunUntil([&]() {
    return EvalJs(content::JsReplace(kFindAndClickDivWithClassScript,
                                     kDivObjectClass))
        .ExtractBool();
  }));

  // After finding and clicking the div, make sure the side panel opens and
  // loaded a result.
  VerifySidePanelLoaded();
}

IN_PROC_BROWSER_TEST_P(LensOverlayLiveTest, ClickObject_SignedOut) {
  // Navigate to a website and wait for paint before starting controller.
  WaitForPaint(kNpsObjectUrl);
  EXPECT_TRUE(content::WaitForLoadStop(web_contents()));

  // State should start in off.
  auto* controller = browser()
                         ->GetTabStripModel()
                         ->GetActiveTab()
                         ->GetTabFeatures()
                         ->lens_overlay_controller();
  ASSERT_EQ(controller->state(), State::kOff);

  auto* search_controller =
      LensSearchController::From(browser()->GetActiveTabInterface());

  // Showing UI should change the state to screenshot and eventually to overlay.
  search_controller->OpenLensOverlay(LensOverlayInvocationSource::kAppMenu);
  ASSERT_EQ(controller->state(), State::kScreenshot);
  ASSERT_TRUE(base::test::RunUntil(
      [&]() { return controller->state() == State::kOverlay; }));
  ASSERT_FALSE(IsLensOverlaySidePanelShowing());
  ASSERT_TRUE(content::WaitForLoadStop(GetOverlayWebContents()));

  // Confirm that the WebUI has reported that it is ready. This means the local
  // DOM should be initialized on our WebUI.
  WaitForHistogram("Lens.Overlay.TimeToWebUIReady");

  // Verify that the page returns objects that is selectable on the overlay.
  ASSERT_TRUE(base::test::RunUntil([&]() {
    return EvalJs(content::JsReplace(kFindAndClickDivWithClassScript,
                                     kDivObjectClass))
        .ExtractBool();
  }));

  // After finding and clicking the div, make sure the side panel opens and
  // loaded a result.
  VerifySidePanelLoaded();
}

// TODO(crbug.com/40066949): Simplify once kSync becomes unreachable or is
// deleted from the codebase. See ConsentLevel::kSync documentation for
// details.
INSTANTIATE_FEATURE_OVERRIDE_TEST_SUITE(LensOverlayLiveTest);

}  // namespace lens
