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

#include "components/live_caption/views/caption_bubble.h"

#include <memory>
#include <utility>

#include "base/cfi_buildflags.h"
#include "base/command_line.h"
#include "base/functional/callback.h"
#include "base/i18n/language_tag.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/bind_post_task.h"
#include "base/task/thread_pool/thread_pool_instance.h"
#include "base/test/bind.h"
#include "base/test/metrics/user_action_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/test/test_timeouts.h"
#include "base/types/expected.h"
#include "build/build_config.h"
#include "caption_bubble_model.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/confirm_bubble.h"
#include "chrome/browser/ui/test/test_browser_ui.h"
#include "chrome/browser/ui/views/accessibility/caption_bubble_context_views.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/live_caption/caption_bubble_context.h"
#include "components/live_caption/caption_bubble_controller.h"
#include "components/live_caption/caption_bubble_settings.h"
#include "components/live_caption/live_caption_bubble_settings.h"
#include "components/live_caption/pref_names.h"
#include "components/live_caption/views/translation_view_wrapper.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "content/public/test/browser_test.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/test/ui_controls.h"

namespace captions {
namespace {

constexpr char kEnglishLanguage[] = "en-US";

class CaptionBubbleBrowserTest : public UiBrowserTest {
 protected:
  CaptionBubbleBrowserTest() {
    scoped_feature_list_.InitWithFeatures({captions::kLiveCaptionScrollable},
                                          {});
  }

  void SetUpOnMainThread() override {
    pref_service_.registry()->RegisterBooleanPref(
        prefs::kLiveCaptionBubbleExpanded, false);
    pref_service_.registry()->RegisterBooleanPref(prefs::kLiveTranslateEnabled,
                                                  false);
    pref_service_.registry()->RegisterBooleanPref(prefs::kLiveCaptionEnabled,
                                                  false);
    pref_service_.registry()->RegisterStringPref(
        prefs::kLiveCaptionLanguageCode, kEnglishLanguage);
    pref_service_.registry()->RegisterLanguageTagPref(
        prefs::kLiveTranslateTargetLanguageCode,
        base::i18n::GetKnownLanguageTag("en"));
    UiBrowserTest::SetUpOnMainThread();
  }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitchASCII("--force-device-scale-factor", "1.01");
    UiBrowserTest::SetUpCommandLine(command_line);
  }

  void TearDownOnMainThread() override {
    context_.reset();
    UiBrowserTest::TearDownOnMainThread();
  }

  // UiBrowserTest:
  void ShowUi(const std::string& name) override {
    context_ = std::make_unique<CaptionBubbleContextViews>(
        browser()->GetActiveTabInterface()->GetContents());
    OnCaptionBubbleClosedCallback callback;
    model_ = std::make_unique<CaptionBubbleModel>(context_.get(),
                                                  std::move(callback));
    settings_ = std::make_unique<LiveCaptionBubbleSettings>(&pref_service_);
    settings_->SetLiveCaptionBubbleExpanded(true);

    const std::string application_locale;
    base::OnceClosure destroyed_callback;
    auto bubble = std::make_unique<CaptionBubble>(
        settings_.get(),
        std::make_unique<TranslationViewWrapper>(settings_.get()),
        application_locale, std::move(destroyed_callback));
    bubble_ = bubble.get();
    views::BubbleDialogDelegateView::CreateBubble(std::move(bubble))->Show();
    bubble_->SetModel(model_.get());

    // Prepare RunLoop,
    run_loop_ = std::make_unique<base::RunLoop>();
    // Increase to 1024u for a manual run to observe scrolling on screen.
    SingleStep(128u);
    // The test will wait until all steps are completed.
    run_loop_->Run();
  }

  // These next two are not necessary if subclassing DialogBrowserTest.
  bool VerifyUi() override {
    views::Widget* widget = GetWidgetForScreenshot();

    auto* const test_info =
        testing::UnitTest::GetInstance()->current_test_info();
    const std::string screenshot_name =
        base::StrCat({test_info->test_suite_name(), "_", test_info->name()});

    return VerifyPixelUi(widget, "CaptureBubblePixelTest", screenshot_name) !=
           ui::test::ActionResult::kFailed;
  }

  void DismissUi() override {
    if (bubble_) {
      bubble_->SetModel(nullptr);
      bubble_ = nullptr;
    }
    IgnoreNetworkServiceCrashes();
  }

  void WaitForUserDismissal() override {
    /* Block until the UI has been dismissed. */
    ui_test_utils::BrowserDestroyedObserver(browser()).Wait();
    if (bubble_) {
      bubble_->SetModel(nullptr);
      bubble_ = nullptr;
    }
    IgnoreNetworkServiceCrashes();
  }

 protected:
  // This method adds one more piece of text to the bubble and then
  // performs scroll to the start and scroll to the end (for testing only).
  // It schedules asynchronous call to itself for the next piece of text,
  // in order to yield the the UI thread and let scrolls repaint the view.
  void SingleStep(uint64_t i) {
    if (!bubble_) {
      return;
    }
    model_->SetPartialText(base::StringPrintf("ABCDEF %ul ", i));
    model_->CommitPartialText();
    auto* const scroll_bar =
        bubble_->GetScrollViewForTesting()->vertical_scroll_bar();
    ASSERT_EQ(scroll_bar->GetOrientation(),
              views::ScrollBar::Orientation::kVertical);

    // Next iteration needs to be reschedued on UI thread, so that views can
    // be repainted.
    auto next_step =
        (i > 1) ? base::BindPostTask(
                      base::SingleThreadTaskRunner::GetCurrentDefault(),
                      base::BindOnce(&CaptionBubbleBrowserTest::SingleStep,
                                     base::Unretained(this), i - 1))
                : base::BindOnce(&base::RunLoop::Quit,
                                 base::Unretained(run_loop_.get()));

    if (scroll_bar->GetVisible()) {
      // If the scrollbar is already visible, reschedule simulated scroll to the
      // beginning, then scroll to the end, and then do the next step.
      // The scrolled view should be repainted after ~every action.
      next_step =
          base::BindPostTask(base::SingleThreadTaskRunner::GetCurrentDefault(),
                             base::BindOnce(
                                 [](views::ScrollBar* scroll_bar) {
                                   ASSERT_TRUE(scroll_bar->ScrollByAmount(
                                       views::ScrollBar::ScrollAmount::kStart));
                                 },
                                 base::Unretained(scroll_bar)))
              .Then(base::BindPostTask(
                  base::SingleThreadTaskRunner::GetCurrentDefault(),
                  base::BindOnce(
                      [](views::ScrollBar* scroll_bar) {
                        ASSERT_TRUE(scroll_bar->ScrollByAmount(
                            views::ScrollBar::ScrollAmount::kEnd));
                      },
                      base::Unretained(scroll_bar))))
              .Then(std::move(next_step));
    }

    std::move(next_step).Run();
  }

  base::test::ScopedFeatureList scoped_feature_list_;

  TestingPrefServiceSimple pref_service_;

  std::unique_ptr<CaptionBubbleContextViews> context_;
  std::unique_ptr<CaptionBubbleModel> model_;
  std::unique_ptr<LiveCaptionBubbleSettings> settings_;
  raw_ptr<CaptionBubble> bubble_;

  std::unique_ptr<base::RunLoop> run_loop_;

 private:
  views::Widget* GetWidgetForScreenshot() const { return bubble_->GetWidget(); }
};

// Test that calls ShowUi("default").
// TODO(crbug.com/422524764): Flakily failing on Windows
#if BUILDFLAG(IS_WIN)
#define MAYBE_InvokeUi_default DISABLED_InvokeUi_default
#else
#define MAYBE_InvokeUi_default InvokeUi_default
#endif
IN_PROC_BROWSER_TEST_F(CaptionBubbleBrowserTest, MAYBE_InvokeUi_default) {
  ShowAndVerifyUi();
}

IN_PROC_BROWSER_TEST_F(CaptionBubbleBrowserTest, InvokeUi_NoWiggleOnResize) {
  ShowUi("NoWiggleOnResize");
  views::Widget* widget = bubble_->GetWidget();
  gfx::Point initial_origin = widget->GetWindowBoundsInScreen().origin();

  model_->SetPartialText("A new line of text.");
  model_->CommitPartialText();

  base::RunLoop().RunUntilIdle();

  gfx::Point new_origin = widget->GetWindowBoundsInScreen().origin();
  EXPECT_EQ(initial_origin, new_origin);
  DismissUi();
}

}  // namespace
}  // namespace captions
