// 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/caption_controller_base.h"

#include <memory>
#include <string>

#include "base/test/run_until.h"
#include "base/test/task_environment.h"
#include "components/live_caption/caption_bubble_context.h"
#include "components/live_caption/caption_bubble_controller.h"
#include "components/live_caption/pref_names.h"
#include "components/live_caption/views/translation_view_wrapper_base.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/pref_service.h"
#include "components/prefs/testing_pref_service.h"
#include "media/mojo/mojom/speech_recognition_result.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

using testing::_;
using testing::Return;

namespace captions {
namespace {

// CaptionControllerBase that we'll instantiate for test.
class TestCaptionControllerBase : public CaptionControllerBase {
 public:
  TestCaptionControllerBase(PrefService* profile_prefs,
                            const std::string& application_locale,
                            std::unique_ptr<Delegate> delegate)
      : CaptionControllerBase(profile_prefs,
                              application_locale,
                              std::move(delegate)) {}

  MOCK_METHOD(CaptionBubbleSettings*, caption_bubble_settings, (), (override));
};

class MockListener : public CaptionControllerBase::Listener {
 public:
  explicit MockListener(bool* free_flag = nullptr) : free_flag_(free_flag) {}
  ~MockListener() override {
    if (free_flag_) {
      *free_flag_ = true;
    }
  }

  MOCK_METHOD(bool,
              OnTranscription,
              (content::RenderFrameHost*,
               CaptionBubbleContext*,
               const media::SpeechRecognitionResult&),
              (override));
  MOCK_METHOD(void,
              OnAudioStreamEnd,
              (content::RenderFrameHost*, CaptionBubbleContext*),
              (override));
  MOCK_METHOD(void,
              OnLanguageIdentificationEvent,
              (content::RenderFrameHost*,
               CaptionBubbleContext*,
               const media::mojom::LanguageIdentificationEventPtr&),
              (override));

 private:
  raw_ptr<bool> free_flag_ = nullptr;
};

class MockCaptionBubbleContext : public CaptionBubbleContext {
 public:
  ~MockCaptionBubbleContext() override = default;

  MOCK_METHOD(void, GetBounds, (GetBoundsCallback), (const override));
  // MOCK_METHOD hates "const std::string" as a return type.
  const std::string GetSessionId() const override { return {}; }
  MOCK_METHOD(void, Activate, (), (override));
  MOCK_METHOD(bool, IsActivatable, (), (const override));
  MOCK_METHOD(bool, ShouldAvoidOverlap, (), (const override));
  MOCK_METHOD(std::unique_ptr<CaptionBubbleSessionObserver>,
              GetCaptionBubbleSessionObserver,
              (),
              (override));
  MOCK_METHOD(OpenCaptionSettingsCallback,
              GetOpenCaptionSettingsCallback,
              (),
              (override));
};

class MockCaptionBubbleController : public CaptionBubbleController {
 public:
  MockCaptionBubbleController() = default;
  ~MockCaptionBubbleController() override = default;

  // CaptionControllerBase::Listener
  MOCK_METHOD(bool,
              OnTranscription,
              (content::RenderFrameHost*,
               CaptionBubbleContext*,
               const media::SpeechRecognitionResult&),
              (override));
  MOCK_METHOD(void,
              OnAudioStreamEnd,
              (content::RenderFrameHost*, CaptionBubbleContext*),
              (override));
  MOCK_METHOD(void,
              OnLanguageIdentificationEvent,
              (content::RenderFrameHost*,
               CaptionBubbleContext*,
               const media::mojom::LanguageIdentificationEventPtr&),
              (override));

  // CaptionBubbleController
  MOCK_METHOD(void,
              OnError,
              (CaptionBubbleContext*,
               CaptionBubbleErrorType,
               OnErrorClickedCallback,
               OnDoNotShowAgainClickedCallback),
              (override));
  MOCK_METHOD(void,
              UpdateCaptionStyle,
              (std::optional<ui::CaptionStyle>),
              (override));
  MOCK_METHOD(bool, IsWidgetVisibleForTesting, (), (override));
  MOCK_METHOD(bool, IsGenericErrorMessageVisibleForTesting, (), (override));
  MOCK_METHOD(std::string, GetBubbleLabelTextForTesting, (), (override));
  MOCK_METHOD(void, CloseActiveModelForTesting, (), (override));
};

class MockCaptionControllerDelegate : public CaptionControllerBase::Delegate {
 public:
  explicit MockCaptionControllerDelegate(
      std::unique_ptr<CaptionBubbleController> bubble_controller) {
    EXPECT_CALL(*this, CreateCaptionBubbleController(_, _, _))
        .WillOnce(Return(std::move(bubble_controller)));
  }
  ~MockCaptionControllerDelegate() override = default;

  MOCK_METHOD(std::unique_ptr<CaptionBubbleController>,
              CreateCaptionBubbleController,
              (CaptionBubbleSettings*,
               const std::string&,
               std::unique_ptr<TranslationViewWrapperBase>),
              (override));

  void AddCaptionStyleObserver(ui::NativeThemeObserver*) override {}

  void RemoveCaptionStyleObserver(ui::NativeThemeObserver*) override {}
};

void RegisterStylePrefs(TestingPrefServiceSimple* pref_service) {
  const std::string kCaptionsTextSize = "20%";
  const std::string kCaptionsTextFont = "aerial";
  const std::string kCaptionsTextColor = "255,99,71";
  const std::string kCaptionsBackgroundColor = "90,255,50";
  const std::string kCaptionsTextShadow = "10px";
  constexpr int kCaptionsTextOpacity = 50;
  constexpr int kCaptionsBackgroundOpacity = 30;

  pref_service->registry()->RegisterStringPref(
      prefs::kAccessibilityCaptionsTextSize, kCaptionsTextSize);
  pref_service->registry()->RegisterStringPref(
      prefs::kAccessibilityCaptionsTextFont, kCaptionsTextFont);
  pref_service->registry()->RegisterStringPref(
      prefs::kAccessibilityCaptionsTextColor, kCaptionsTextColor);
  pref_service->registry()->RegisterIntegerPref(
      prefs::kAccessibilityCaptionsTextOpacity, kCaptionsTextOpacity);
  pref_service->registry()->RegisterStringPref(
      prefs::kAccessibilityCaptionsBackgroundColor, kCaptionsBackgroundColor);
  pref_service->registry()->RegisterStringPref(
      prefs::kAccessibilityCaptionsTextShadow, kCaptionsTextShadow);
  pref_service->registry()->RegisterIntegerPref(
      prefs::kAccessibilityCaptionsBackgroundOpacity,
      kCaptionsBackgroundOpacity);
}

class CaptionControllerBaseTest : public testing::Test {
 public:
  ~CaptionControllerBaseTest() override = default;

  void SetUp() override {
    testing::Test::SetUp();

    RegisterStylePrefs(&testing_pref_service_);
  }

  std::unique_ptr<TestCaptionControllerBase> CreateController(
      std::unique_ptr<CaptionControllerBase::Delegate> delegate = nullptr) {
    return std::make_unique<TestCaptionControllerBase>(
        &testing_pref_service_, speech::kUsEnglishLocale, std::move(delegate));
  }

  TestingPrefServiceSimple testing_pref_service_;
  base::test::TaskEnvironment task_environment_;
};

TEST_F(CaptionControllerBaseTest, ListenersAreFreedOnDestruction) {
  bool was_freed = false;
  auto listener = std::make_unique<MockListener>(&was_freed);

  auto controller_under_test = CreateController();
  controller_under_test->AddListener(std::move(listener));
  EXPECT_FALSE(was_freed);
  controller_under_test.reset();
  EXPECT_TRUE(was_freed);
}

TEST_F(CaptionControllerBaseTest, CaptionBubbleControllerReceivesCallbacks) {
  auto mock_bubble_controller = std::make_unique<MockCaptionBubbleController>();
  auto* mock_bubble_controller_raw = mock_bubble_controller.get();
  auto controller_under_test =
      CreateController(std::make_unique<MockCaptionControllerDelegate>(
          std::move(mock_bubble_controller)));
  controller_under_test->create_ui_for_testing();
  EXPECT_CALL(*mock_bubble_controller_raw, OnAudioStreamEnd(nullptr, nullptr));
  controller_under_test->OnAudioStreamEnd(nullptr, nullptr);
}

TEST_F(CaptionControllerBaseTest, CaptionBubbleAliasIsAddedAndRemoved) {
  auto mock_bubble_controller = std::make_unique<MockCaptionBubbleController>();
  auto* mock_bubble_controller_raw = mock_bubble_controller.get();
  auto controller_under_test =
      CreateController(std::make_unique<MockCaptionControllerDelegate>(
          std::move(mock_bubble_controller)));

  // Create the UI, and expect that its alias is now correct.
  controller_under_test->create_ui_for_testing();
  EXPECT_EQ(mock_bubble_controller_raw,
            controller_under_test->caption_bubble_controller_for_testing());

  // Delete the UI, and expect that its alias is now cleared.
  controller_under_test->destroy_ui_for_testing();
  EXPECT_EQ(nullptr,
            controller_under_test->caption_bubble_controller_for_testing());
}

TEST_F(CaptionControllerBaseTest, ListenersReceiveTranscription) {
  MockCaptionBubbleContext context;

  auto listener = std::make_unique<MockListener>();
  content::RenderFrameHost* rfh =
      reinterpret_cast<content::RenderFrameHost*>(listener.get());
  EXPECT_CALL(*listener, OnAudioStreamEnd(rfh, &context));

  auto controller_under_test = CreateController();
  controller_under_test->AddListener(std::move(listener));
  controller_under_test->OnAudioStreamEnd(rfh, &context);
}

TEST_F(CaptionControllerBaseTest, TranscriptionStopsIfNoListeners) {
  MockCaptionBubbleContext context;
  media::SpeechRecognitionResult result;

  auto controller_under_test = CreateController();
  EXPECT_FALSE(controller_under_test->DispatchTranscription(
      /*rfh=*/nullptr, &context, result));
}

TEST_F(CaptionControllerBaseTest, ListenersReceiveAudioEnd) {
  MockCaptionBubbleContext context;
  media::SpeechRecognitionResult result;

  auto listener = std::make_unique<MockListener>();
  content::RenderFrameHost* rfh =
      reinterpret_cast<content::RenderFrameHost*>(listener.get());
  EXPECT_CALL(*listener, OnTranscription(rfh, &context, result));

  auto controller_under_test = CreateController();
  controller_under_test->AddListener(std::move(listener));
  controller_under_test->DispatchTranscription(rfh, &context, result);
}

class CaptionControllerBaseListenerTest : public CaptionControllerBaseTest {
 public:
  class TestCaptionController : public CaptionControllerBase {
   public:
    TestCaptionController(PrefService* profile_prefs,
                          const std::string& application_locale)
        : CaptionControllerBase(profile_prefs, application_locale, nullptr) {}
    ~TestCaptionController() override = default;

    MOCK_METHOD(void, OnFirstListenerAdded, (), (override));
    MOCK_METHOD(void, OnLastListenerRemoved, (), (override));
    MOCK_METHOD(CaptionBubbleSettings*,
                caption_bubble_settings,
                (),
                (override));
  };
};

TEST_F(CaptionControllerBaseListenerTest, OnFirstListenerAddedCalled) {
  auto controller = std::make_unique<TestCaptionController>(
      &testing_pref_service_, speech::kUsEnglishLocale);
  EXPECT_CALL(*controller, OnFirstListenerAdded).Times(1);
  EXPECT_CALL(*controller, OnLastListenerRemoved).Times(0);

  auto listener1 = std::make_unique<MockListener>();
  controller->AddListener(std::move(listener1));

  // Adding a second listener should not trigger the callback.
  auto listener2 = std::make_unique<MockListener>();
  controller->AddListener(std::move(listener2));
}

TEST_F(CaptionControllerBaseListenerTest, OnLastListenerRemovedCalled) {
  auto controller = std::make_unique<TestCaptionController>(
      &testing_pref_service_, speech::kUsEnglishLocale);
  EXPECT_CALL(*controller, OnFirstListenerAdded).Times(1);
  EXPECT_CALL(*controller, OnLastListenerRemoved).Times(1);

  auto listener1 = std::make_unique<MockListener>();
  auto* listener1_ptr = listener1.get();
  controller->AddListener(std::move(listener1));

  auto listener2 = std::make_unique<MockListener>();
  auto* listener2_ptr = listener2.get();
  controller->AddListener(std::move(listener2));

  // Removing the first listener should not trigger the callback.
  controller->remove_listener_for_testing(listener1_ptr);

  // Removing the last listener should trigger the callback.
  controller->remove_listener_for_testing(listener2_ptr);
}

TEST_F(CaptionControllerBaseListenerTest, RemoveSoonRemovesListener) {
  auto controller = std::make_unique<TestCaptionController>(
      &testing_pref_service_, speech::kUsEnglishLocale);
  EXPECT_CALL(*controller, OnFirstListenerAdded).Times(1);
  EXPECT_CALL(*controller, OnLastListenerRemoved).Times(1);

  bool was_freed = false;
  auto listener = std::make_unique<MockListener>(&was_freed);
  auto* listener_ptr = listener.get();
  controller->AddListener(std::move(listener));

  EXPECT_FALSE(was_freed);
  controller->RemoveSoon(listener_ptr);
  EXPECT_FALSE(was_freed);  // Not freed yet

  // Run pending tasks to process removal.
  EXPECT_TRUE(base::test::RunUntil([&]() { return was_freed; }));
}

}  // namespace
}  // namespace captions
