// 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 <iterator>
#include <memory>
#include <string>

#include "base/command_line.h"
#include "base/containers/fixed_flat_set.h"
#include "base/files/file_util.h"
#include "base/logging.h"
#include "base/memory/weak_ptr.h"
#include "base/native_library.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/strings/strcat.h"
#include "base/strings/stringprintf.h"
#include "base/task/current_thread.h"
#include "base/test/bind.h"
#include "base/test/scoped_feature_list.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/browsing_data/chrome_browsing_data_remover_constants.h"
#include "chrome/browser/component_updater/translate_kit_component_installer.h"
#include "chrome/browser/on_device_translation/service_controller_manager_factory.h"
#include "chrome/browser/on_device_translation/test/test_util.h"
#include "chrome/browser/on_device_translation/translation_manager_impl.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/crx_file/id_util.h"
#include "components/language/core/browser/pref_names.h"
#include "components/on_device_translation/component_manager.h"
#include "components/on_device_translation/constants.h"
#include "components/on_device_translation/features.h"
#include "components/on_device_translation/installer.h"
#include "components/on_device_translation/public/language_pack.h"
#include "components/on_device_translation/public/pref_names.h"
#include "components/on_device_translation/service/test/test_util.h"
#include "components/on_device_translation/service_controller.h"
#include "components/on_device_translation/service_controller_manager.h"
#include "components/on_device_translation/test/fake_installer.h"
#include "components/optimization_guide/core/model_execution/test/fake_component_update_service.h"
#include "components/prefs/pref_service.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/browsing_data_remover_test_util.h"
#include "content/public/test/url_loader_interceptor.h"
#include "net/base/filename_util.h"
#include "net/dns/mock_host_resolver.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/features_generated.h"
#include "third_party/blink/public/mojom/on_device_translation/translation_manager.mojom.h"

using ::blink::mojom::CanCreateTranslatorResult;
using ::blink::mojom::TranslatorLanguageCode;
using ::content::JsReplace;
using ::optimization_guide::FakeComponent;
using ::optimization_guide::FakeComponentUpdateService;
using ::testing::_;

namespace on_device_translation {

namespace {

constexpr auto kLanguagePackKeys = base::MakeFixedFlatSet<LanguagePackKey>({
    LanguagePackKey::kEn_Es, LanguagePackKey::kEn_Ja,
    LanguagePackKey::kAr_En, LanguagePackKey::kBn_En,
    LanguagePackKey::kDe_En, LanguagePackKey::kEn_Fr,
    LanguagePackKey::kEn_Hi, LanguagePackKey::kEn_It,
    LanguagePackKey::kEn_Ko, LanguagePackKey::kEn_Nl,
    LanguagePackKey::kEn_Pl, LanguagePackKey::kEn_Pt,
    LanguagePackKey::kEn_Ru, LanguagePackKey::kEn_Th,
    LanguagePackKey::kEn_Tr, LanguagePackKey::kEn_Vi,
    LanguagePackKey::kEn_Zh, LanguagePackKey::kEn_ZhHant,
    LanguagePackKey::kBg_En, LanguagePackKey::kCs_En,
    LanguagePackKey::kDa_En, LanguagePackKey::kEl_En,
    LanguagePackKey::kEn_Fi, LanguagePackKey::kEn_Hr,
    LanguagePackKey::kEn_Hu, LanguagePackKey::kEn_Id,
    LanguagePackKey::kEn_Iw, LanguagePackKey::kEn_Lt,
    LanguagePackKey::kEn_No, LanguagePackKey::kEn_Ro,
    LanguagePackKey::kEn_Sk, LanguagePackKey::kEn_Sl,
    LanguagePackKey::kEn_Sv, LanguagePackKey::kEn_Uk,
    LanguagePackKey::kEn_Kn, LanguagePackKey::kEn_Ta,
    LanguagePackKey::kEn_Te, LanguagePackKey::kEn_Mr,
});
static_assert(std::size(kLanguagePackKeys) ==
              static_cast<size_t>(LanguagePackKey::kMaxValue) + 1);

std::string GetPreferredLanguageString(
    const base::span<const LanguagePackKey>& language_pack_keys) {
  // Get unique set of language codes from the keys.
  std::set<std::string_view> language_codes;
  for (const auto& language_pack_key : language_pack_keys) {
    language_codes.insert(GetSourceLanguageCode(language_pack_key));
    language_codes.insert(GetTargetLanguageCode(language_pack_key));
  }

  // Create a preferred string
  std::string selected_languages = "";
  for (auto language_code : language_codes) {
    selected_languages += language_code;
    selected_languages += ",";
  }

  // Remove the extra comma at the end.
  selected_languages.pop_back();

  return selected_languages;
}

// Handles HTTP requests to `path` with `content` as the response body.
// `content` is expected to be JavaScript; the response mime type is always
// set to "text/javascript". Invokes `done_callback` after serving the HTTP
// request.
std::unique_ptr<net::test_server::HttpResponse> RespondWithJS(
    const std::string& path,
    const std::string& content,
    base::OnceClosure done_callback,
    const net::test_server::HttpRequest& request) {
  GURL request_url = request.GetURL();
  if (request_url.GetPath() != path) {
    return nullptr;
  }

  auto response = std::make_unique<net::test_server::BasicHttpResponse>();
  response->set_content_type("text/javascript");
  response->set_content(content);
  std::move(done_callback).Run();
  return response;
}

// Sets the path of the mock library to the command line.
void SetMockLibraryPathToCommandLine(base::CommandLine* command_line) {
  command_line->AppendSwitchPath(kTranslateKitBinaryPath, GetMockLibraryPath());
}

// Writes fake dictionary data to a file and sets the path of the file to the
// command line.
void WriteFakeDictionaryDataAndSetCommandLine(LanguagePackKey key,
                                              const base::FilePath& temp_dir,
                                              base::CommandLine* command_line) {
  const auto dict_dir_path =
      temp_dir.AppendASCII(GetPackageInstallDirName(key));
  const auto dict_path = dict_dir_path.AppendASCII("dict.dat");
  CHECK(base::CreateDirectory(dict_dir_path));
  CHECK(base::File(dict_path, base::File::FLAG_CREATE | base::File::FLAG_WRITE)
            .WriteAndCheck(0, base::as_byte_span(CreateFakeDictionaryData(
                                  GetSourceLanguageCode(key),
                                  GetTargetLanguageCode(key)))));
  command_line->AppendSwitchASCII(
      "translate-kit-packages",
      base::StrCat({GetSourceLanguageCode(key), ",", GetTargetLanguageCode(key),
                    ",", dict_dir_path.AsUTF8Unsafe()}));
}

// Returns a string representation of the result of TranslationAvailable().
std::string_view GetCanCreateTranslatorResultString(
    CanCreateTranslatorResult result) {
  switch (result) {
    case CanCreateTranslatorResult::kReadily:
      return "available";
    case CanCreateTranslatorResult::kAfterDownloadLibraryNotReady:
    case CanCreateTranslatorResult::kAfterDownloadLanguagePackNotReady:
    case CanCreateTranslatorResult::
        kAfterDownloadLibraryAndLanguagePackNotReady:
    case CanCreateTranslatorResult::kAfterDownloadTranslatorCreationRequired:
      return "downloadable";
    case CanCreateTranslatorResult::kNoNotSupportedLanguage:
    case CanCreateTranslatorResult::kNoServiceCrashed:
    case CanCreateTranslatorResult::kNoDisallowedByPolicy:
    case CanCreateTranslatorResult::kNoExceedsServiceCountLimitation:
    case CanCreateTranslatorResult::kNoInvalidStoragePartition:
      return "unavailable";
  }
}

void Sleep(base::TimeDelta delay) {
  base::RunLoop loop;
  base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, loop.QuitClosure(), delay);
  loop.Run();
}

// An implementation of SupportsUserData to be used in tests.
class TestSupportsUserData : public base::SupportsUserData {
 public:
  TestSupportsUserData() = default;
  ~TestSupportsUserData() override = default;
};

}  // namespace

class OnDeviceTranslationBrowserTest : public InProcessBrowserTest {
 public:
  OnDeviceTranslationBrowserTest() {
    scoped_feature_list_.InitAndEnableFeature(blink::features::kTranslationAPI);
    CHECK(tmp_dir_.CreateUniqueTempDir());
  }
  ~OnDeviceTranslationBrowserTest() override = default;

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    base::FilePath test_data_dir;
    base::PathService::Get(chrome::DIR_TEST_DATA, &test_data_dir);
    embedded_https_test_server().ServeFilesFromDirectory(test_data_dir);
    ASSERT_TRUE(embedded_https_test_server().Start());

    // Inject the adapter.
    on_device_translation::ServiceControllerManagerFactory::GetInstance()
        ->Get(GetBrowserContext())
        ->SetInstallerForTesting(&adapter_);
  }

 protected:
  const base::FilePath& GetTempDir() { return tmp_dir_.GetPath(); }

  content::BrowserContext* GetBrowserContext() {
    return browser()
        ->tab_strip_model()
        ->GetActiveWebContents()
        ->GetBrowserContext();
  }

  const url::Origin GetLastCommittedOrigin() {
    return browser()
        ->tab_strip_model()
        ->GetActiveWebContents()
        ->GetPrimaryMainFrame()
        ->GetLastCommittedOrigin();
  }

  content::RenderProcessHost* GetRenderProcessHost() {
    return browser()
        ->tab_strip_model()
        ->GetActiveWebContents()
        ->GetPrimaryMainFrame()
        ->GetProcess();
  }

  // Navigates to an empty page.
  void NavigateToEmptyPage() {
    CHECK(ui_test_utils::NavigateToURL(
        browser(), embedded_https_test_server().GetURL("/empty.html")));
  }

  // Sets the SelectedLanguages prefs to the given value. This will change the
  // AcceptLanguages pref.
  void SetSelectedLanguages(const std::string_view value) {
    browser()->GetProfile()->GetPrefs()->SetString(
        language::prefs::kSelectedLanguages, value);
  }

  // Sets the SelectedLanguages prefs to support all the languages in the
  // `language_pack_keys`. This will change the AcceptLanguages pref.
  void SetSelectedLanguages(
      const base::span<const LanguagePackKey>& language_pack_keys) {
    SetSelectedLanguages(GetPreferredLanguageString(language_pack_keys));
  }

  // Tests the behavior of availability().
  void TestCanTranslateResult(const std::string_view sourceLang,
                              const std::string_view targetLang,
                              CanCreateTranslatorResult expected_result) {
    NavigateToEmptyPage();

    content::RenderProcessHost* render_process_host = GetRenderProcessHost();
    CHECK(render_process_host);

    // Call TranslationAvailable() via mojo interface to verify the detailed
    // result.
    mojo::Remote<blink::mojom::TranslationManager> remote;
    TestSupportsUserData fake_user_data;
    TranslationManagerImpl::Bind(render_process_host, GetBrowserContext(),
                                 &fake_user_data, GetLastCommittedOrigin(),
                                 g_browser_process->component_updater(),
                                 remote.BindNewPipeAndPassReceiver());
    base::RunLoop run_loop;
    remote->TranslationAvailable(
        TranslatorLanguageCode::New(std::string(sourceLang)),
        TranslatorLanguageCode::New(std::string(targetLang)),
        base::BindLambdaForTesting([&](CanCreateTranslatorResult result) {
          EXPECT_EQ(result, expected_result);
          run_loop.Quit();
        }));
    run_loop.Run();

    // Need to navigate to an empty page to reset the state of the
    // TranslationManagerImpl.
    NavigateToEmptyPage();
    // Calls TranslationAvailable() via JS API (Translator.availability()) to
    // verify the result string.
    TestTranslationAvailable(
        browser(), sourceLang, targetLang,
        GetCanCreateTranslatorResultString(expected_result));
  }

  content::EvalJsResult EvalJs(
      std::string_view script,
      Browser* target_browser = nullptr,
      int options = content::EXECUTE_SCRIPT_DEFAULT_OPTIONS) {
    return content::EvalJs((target_browser ? target_browser : browser())
                               ->tab_strip_model()
                               ->GetActiveWebContents(),
                           script, options);
  }

  testing::AssertionResult ExecJs(
      std::string_view script,
      Browser* target_browser = nullptr,
      int options = content::EXECUTE_SCRIPT_DEFAULT_OPTIONS) {
    return content::ExecJs((target_browser ? target_browser : browser())
                               ->tab_strip_model()
                               ->GetActiveWebContents(),
                           script, options);
  }

  // Evaluates the given script and returns the result string. If the script
  // throws an error, returns the error message.
  // When `target_browser` is not nullptr, the script is evaluated in the
  // context of the given browser, otherwise the script is evaluated in the
  // context of the default browser.
  std::string EvalJsCatchingError(
      std::string_view script,
      Browser* target_browser = nullptr,
      int options = content::EXECUTE_SCRIPT_DEFAULT_OPTIONS) {
    return EvalJs(base::StringPrintf(R"(
      (async () => {
        try {
          %s
        } catch (e) {
          return e.toString();
        }
      })();
    )",
                                     script),
                  target_browser, options)
        .ExtractString();
  }

  // Creates a console observer for the given pattern. When `target_browser`
  // is not nullptr, the observer is created in the context of the given
  // browser, otherwise the observer is created in the context of the default
  // browser.
  std::unique_ptr<content::WebContentsConsoleObserver> CreateConsoleObserver(
      const std::string_view pattern,
      Browser* target_browser = nullptr) {
    auto observer = std::make_unique<content::WebContentsConsoleObserver>(
        (target_browser ? target_browser : browser())
            ->tab_strip_model()
            ->GetActiveWebContents());
    observer->SetPattern(std::string(pattern));
    return observer;
  }

  // Waits for the console observer to receive a message.
  void WaitForConsoleObserver(
      content::WebContentsConsoleObserver& console_observer) {
    ASSERT_TRUE(console_observer.Wait());
    EXPECT_FALSE(console_observer.messages().empty());
  }

  void ClearSiteContentSettings() {
    content::BrowsingDataRemover* remover =
        browser()->GetProfile()->GetBrowsingDataRemover();
    content::BrowsingDataRemoverCompletionObserver observer(remover);
    remover->RemoveAndReply(
        base::Time(), base::Time::Max(),
        chrome_browsing_data_remover::DATA_TYPE_CONTENT_SETTINGS,
        chrome_browsing_data_remover::ALL_ORIGIN_TYPES, &observer);
    observer.BlockUntilCompletion();
  }

  content::RenderFrameHost* CreateIframe(Browser* target_browser = nullptr) {
    EXPECT_EQ(EvalJsCatchingError(R"(
      window._iframe = document.createElement('iframe');
      document.body.appendChild(window._iframe);
      return "OK";
  )",
                                  target_browser),
              "OK");

    return ChildFrameAt((target_browser ? target_browser : browser())
                            ->tab_strip_model()
                            ->GetActiveWebContents(),
                        0);
  }

  bool RemoveIframe(Browser* target_browser = nullptr) {
    return ExecJs("document.body.removeChild(window._iframe);");
  }

 private:
  base::ScopedTempDir tmp_dir_;
  base::test::ScopedFeatureList scoped_feature_list_;
  TestInstallerAdapter adapter_;
};

// Tests the behavior of create() when the library is installed before
// the language pack.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CreateTranslatorInstallLibraryAndThenLanguagePack) {
  MockComponentManager mock_component_manager(GetTempDir());
  NavigateToEmptyPage();

  base::RunLoop run_loop_for_register_translate_kit;
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .WillOnce([&]() { run_loop_for_register_translate_kit.Quit(); });

  base::RunLoop run_loop_for_register_language_pack;
  EXPECT_CALL(mock_component_manager,
              RegisterTranslateKitLanguagePackComponent(_))
      .WillOnce([&](LanguagePackKey key) {
        EXPECT_EQ(key, LanguagePackKey::kEn_Ja);
        run_loop_for_register_language_pack.Quit();
      });

  // Create a translator.
  EXPECT_EQ(EvalJsCatchingError(R"(
      window._testPromise = Translator.create({
          sourceLanguage: 'en',
          targetLanguage: 'ja',
        });
      window._testPromiseResolved = false;
      window._testPromise.then(() => {
        window._testPromiseResolved = true;
      });
      return 'OK';
  )"),
            "OK");

  // Wait until RegisterTranslateKitComponentImpl() is called.
  run_loop_for_register_translate_kit.Run();
  // Wait until RegisterTranslateKitLanguagePackComponent() is called.
  run_loop_for_register_language_pack.Run();
  // Install the mock TranslateKit component.
  mock_component_manager.InstallMockTranslateKitComponent();

  // The promise of create() should not be resolved yet.
  EXPECT_FALSE(EvalJs("window._testPromiseResolved").ExtractBool());

  // Install the mock language pack.
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);

  // Translate "hello" to Japanese.
  // Note: the mock TranslateKit component returns the concatenation of the
  // content of "dict.dat" in the language pack and the input text.
  // See comments in mock_translate_kit_lib.cc for more details.
  EXPECT_EQ(EvalJsCatchingError(
                "return await (await window._testPromise).translate('hello');"),
            "en to ja: hello");
}

// Tests the behavior of create() when the language pack is installed
// before the library.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CreateTranslatorInstallLanguagePackAndThenLibrary) {
  MockComponentManager mock_component_manager(GetTempDir());
  NavigateToEmptyPage();

  base::RunLoop run_loop_for_register_translate_kit;
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .WillOnce([&]() { run_loop_for_register_translate_kit.Quit(); });

  base::RunLoop run_loop_for_register_language_pack;
  EXPECT_CALL(mock_component_manager,
              RegisterTranslateKitLanguagePackComponent(_))
      .WillOnce([&](LanguagePackKey key) {
        EXPECT_EQ(key, LanguagePackKey::kEn_Ja);
        run_loop_for_register_language_pack.Quit();
      });

  // Create a translator.
  EXPECT_EQ(EvalJsCatchingError(R"(
      window._testPromise = Translator.create({
          sourceLanguage: 'en',
          targetLanguage: 'ja',
        });
      window._testPromiseResolved = false;
      window._testPromise.then(() => {
        window._testPromiseResolved = true;
      });
      return 'OK';
  )"),
            "OK");

  // Wait until RegisterTranslateKitComponentImpl() is called.
  run_loop_for_register_translate_kit.Run();
  // Wait until RegisterTranslateKitLanguagePackComponent() is called.
  run_loop_for_register_language_pack.Run();

  // Install the mock language pack.
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);

  // The promise of create() should not be resolved yet.
  EXPECT_FALSE(EvalJs("window._testPromiseResolved").ExtractBool());

  // Install the mock TranslateKit component.
  mock_component_manager.InstallMockTranslateKitComponent();
  // Translate "hello" to Japanese.
  EXPECT_EQ(EvalJsCatchingError(
                "return await (await window._testPromise).translate('hello');"),
            "en to ja: hello");
}

// TODO(crbug.com/421947718): Disabled because there's a race between triggering
// user activation and consuming it when calling `create` multiple times.
//
// Tests the behavior of multiple create() calls with different
// source/target languages.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       DISABLED_CreateTranslatorInstallMultipleLanguagePacks) {
  MockComponentManager mock_component_manager(GetTempDir());
  NavigateToEmptyPage();

  base::RunLoop run_loop_for_register_translate_kit;
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .WillOnce([&]() { run_loop_for_register_translate_kit.Quit(); });

  base::RunLoop run_loop_for_register_en_ja_language_pack;
  base::RunLoop run_loop_for_register_en_es_language_pack;
  EXPECT_CALL(mock_component_manager,
              RegisterTranslateKitLanguagePackComponent(_))
      .WillOnce([&](LanguagePackKey key) {
        EXPECT_EQ(key, LanguagePackKey::kEn_Ja);
        // Call RegisterLanguagePack() to avoid redundant calls of
        // RegisterTranslateKitLanguagePackComponent().
        mock_component_manager.RegisterLanguagePack(key);
        run_loop_for_register_en_ja_language_pack.Quit();
      })
      .WillOnce([&](LanguagePackKey key) {
        EXPECT_EQ(key, LanguagePackKey::kEn_Es);
        mock_component_manager.RegisterLanguagePack(key);
        run_loop_for_register_en_es_language_pack.Quit();
      });

  // Helper function to get the state of a promise at the moment the helper
  // function is called.
  EXPECT_TRUE(ExecJs(R"(
    self.getPromiseState = async promise => {
        const symbol = Symbol();
        try {
          const result = await Promise.race([promise, Promise.resolve(symbol)]);
          return result == symbol ? "pending" : "fulfilled";
        } catch (e) {
          return "rejected";
        }
    }
  )"));

  // Create create() multiple times.
  //   1. En => Ja.
  //   2. En => Es.
  //   3. En => Ja.
  EXPECT_TRUE(ExecJs(R"(
      self.enJaPromise1 = Translator.create({
          sourceLanguage: 'en',
          targetLanguage: 'ja',
        });
  )",
                     browser(), content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
  EXPECT_TRUE(ExecJs(R"(
      self.enEsPromise = Translator.create({
          sourceLanguage: 'en',
          targetLanguage: 'es',
        });
  )",
                     browser(), content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));
  EXPECT_TRUE(ExecJs(R"(
      self.enJaPromise2 = Translator.create({
          sourceLanguage: 'en',
          targetLanguage: 'ja',
        });
  )",
                     browser(), content::EXECUTE_SCRIPT_NO_RESOLVE_PROMISES));

  // Wait until RegisterTranslateKitComponentImpl() is called.
  run_loop_for_register_translate_kit.Run();
  // Wait until RegisterTranslateKitLanguagePackComponent() is called for
  // `en_ja` and `en_es`.
  run_loop_for_register_en_ja_language_pack.Run();
  run_loop_for_register_en_es_language_pack.Run();

  // Install the mock TranslateKit component.
  mock_component_manager.InstallMockTranslateKitComponent();

  // All promises should not be resolved yet.
  EXPECT_EQ(EvalJs("getPromiseState(enJaPromise1)").ExtractString(), "pending");
  EXPECT_EQ(EvalJs("getPromiseState(enEsPromise)").ExtractString(), "pending");
  EXPECT_EQ(EvalJs("getPromiseState(enJaPromise2)").ExtractString(), "pending");

  // Install the mock `en_ja` language pack.
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);

  // Translate to Japanese. Both `en_ja` promises should be resolved now.
  EXPECT_EQ(EvalJsCatchingError(
                "return await (await enJaPromise1).translate('hello');"),
            "en to ja: hello");
  EXPECT_EQ(
      EvalJsCatchingError("return await (await enJaPromise2).translate('hi');"),
      "en to ja: hi");

  // The promise of `en_es` should not be resolved yet.
  EXPECT_EQ(EvalJs("getPromiseState(enEsPromise)").ExtractString(), "pending");

  // Install the mock `en_es` language pack.
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Es);

  // Translate to Spanish. The `en_es` promise should be resolved now.
  EXPECT_EQ(EvalJsCatchingError(
                "return await (await self.enEsPromise).translate('hello');"),
            "en to es: hello");
}

// TODO(crbug.com/421947718): Disabled because there's a race between triggering
// user activation and consuming it when calling `create` multiple times.
//
// Tests the behavior of create() when the number of pending tasks
// exceeds the limit.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       DISABLED_ExceedMaxPendingTaskCount) {
  MockComponentManager mock_component_manager(GetTempDir());
  NavigateToEmptyPage();

  base::RunLoop run_loop_for_register_translate_kit;
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .WillOnce([&]() { run_loop_for_register_translate_kit.Quit(); });

  base::RunLoop run_loop_for_register_language_pack;
  EXPECT_CALL(mock_component_manager,
              RegisterTranslateKitLanguagePackComponent(_))
      .WillOnce([&](LanguagePackKey key) {
        EXPECT_EQ(key, LanguagePackKey::kEn_Ja);
        // Call RegisterLanguagePack() to avoid redundant calls of
        // RegisterTranslateKitLanguagePackComponent().
        mock_component_manager.RegisterLanguagePack(key);
        run_loop_for_register_language_pack.Quit();
      });

  // TODO(crbug.com/421947718): Each `Translator.create` call should be in it's
  // own `EvalJs` call like
  // `CreateTranslator_Delay_ForTranslatorCreatedDuringInitialTranslatorCreationWithDelay`,
  // but since we're blocked on the race issue from crbug.com/421947718, this
  // hasn't been updated yet.
  //
  // Call create() kMaxPendingTaskCount times.
  EXPECT_EQ(EvalJsCatchingError(base::StringPrintf(R"(
      window._testPromises = [];
      window._testPromisesResolved = false;
      const kMaxPendingTaskCount = %zd;
      for (let i = 0; i < kMaxPendingTaskCount; ++i) {
        const promise = Translator.create({
          sourceLanguage: 'en',
          targetLanguage: 'ja',
        });
        promise.then(() => {
          window._testPromisesResolved = true;
        });
        window._testPromises.push(promise);
      }
      return 'OK';
  )",
                                                   kMaxPendingTaskCount)),
            "OK");
  // Wait until RegisterTranslateKitComponentImpl() is called.
  run_loop_for_register_translate_kit.Run();
  // Wait until RegisterTranslateKitLanguagePackComponent() is called.
  run_loop_for_register_language_pack.Run();
  // Install the mock TranslateKit component.
  mock_component_manager.InstallMockTranslateKitComponent();

  // Any promise should not be resolved yet.
  EXPECT_FALSE(EvalJs("window._testPromisesResolved").ExtractBool());

  auto console_observer =
      CreateConsoleObserver("Too many Translator API requests are queued.");

  // Calling create() one more time fails.
  EXPECT_EQ(EvalJsCatchingError(R"(
      await Translator.create({
          sourceLanguage: 'en',
          targetLanguage: 'ja',
        });
      return 'OK';
  )"),
            "NotSupportedError: Unable to create translator for the given "
            "source and target language.");

  // The console message should be logged.
  WaitForConsoleObserver(*console_observer);

  // The all 1024 promises should not be resolved yet.
  EXPECT_FALSE(EvalJs("window._testPromisesResolved").ExtractBool());

  // Install the mock language pack.
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);

  // All promises should be resolved now. The all 1024 translators should
  // be able to translate.
  EXPECT_EQ(EvalJsCatchingError(R"(
      const translators = await Promise.all(window._testPromises);
      const results = await Promise.all(translators.map((translator) => {
        return translator.translate('hello');
      }));
      for (const result of results) {
        if (result != 'en to ja: hello') {
          return `Unexpected result ${result}`;
        }
      }
      return 'OK';
  )"),
            "OK");
}

// Tests the behavior of the failure of translation.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest, TranslationFailure) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});

  NavigateToEmptyPage();

  // Tries to translate "SIMULATE_ERROR" to Japanese. This "SIMULATE_ERROR"
  // causes failure in the mock TranslateKit component. See comments in
  // mock_translate_kit_lib.cc.

  EXPECT_EQ(EvalJsCatchingError(R"(
      const translator = await Translator.create({
        sourceLanguage: 'en',
        targetLanguage: 'ja',
      });
      return await translator.translate('SIMULATE_ERROR');
  )"),
            "UnknownError: Other generic failures occurred.");
}

// Tests the behavior of the crash of calling translate().
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest, CrashWhileTranslating) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});

  NavigateToEmptyPage();

  // Tries to translate "CAUSE_CRASH" to Japanese. This "CAUSE_CRASH" causes
  // a crash in the mock TranslateKit component. See comments in
  // mock_translate_kit_lib.cc.
  EXPECT_EQ(EvalJsCatchingError(R"(
      window._translator = await Translator.create({
        sourceLanguage: 'en',
        targetLanguage: 'ja',
      });
      return await window._translator.translate('CAUSE_CRASH');
  )"),
            "UnknownError: Other generic failures occurred.");

  // After the crash, the translator is not usable.
  EXPECT_EQ(EvalJsCatchingError(
                "return await window._translator.translate('hello');"),
            "UnknownError: Other generic failures occurred.");

  // But a new translator can be created and used.
  TestSimpleTranslationWorks(browser(), "en", "ja");
}

// Tests the behavior of failing to load the library
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CreateTranslatorErrorLibraryLoadFailed) {
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(1);
  mock_component_manager.InstallEmptyMockComponent();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});
  NavigateToEmptyPage();

  auto console_observer =
#if BUILDFLAG(IS_WIN)
      // On Windows, the service crashes when failed to preload the library
      // in PreLockdownSandboxHook().
      CreateConsoleObserver("The translation service crashed.");
#else
      CreateConsoleObserver("Failed to load the translation library.");
#endif  // BUILDFLAG(IS_WIN)

  EXPECT_EQ(EvalJsCatchingError(R"(
            const translator = await Translator.create({
              sourceLanguage: 'en',
              targetLanguage: 'ja',
            });
      )"),
            "NotSupportedError: Unable to create translator for the given "
            "source and target language.");

  // The console message should be logged.
  WaitForConsoleObserver(*console_observer);
}

// Tests that a relative library path causes a crash/failure (reproducing the
// bug/crash in PreLockdownSandboxHook on Windows).
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CreateTranslatorErrorLibraryPathRelative) {
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(1);

  // Set the TranslateKit binary path to a relative path.
  g_browser_process->local_state()->SetFilePath(
      prefs::kTranslateKitBinaryPath,
      base::FilePath(FILE_PATH_LITERAL("relative/path/to/fakefile")));

  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});
  NavigateToEmptyPage();

  EXPECT_EQ(EvalJsCatchingError(R"(
            const translator = await Translator.create({
              sourceLanguage: 'en',
              targetLanguage: 'ja',
            });
      )"),
            "NotSupportedError: Unable to create translator for the given "
            "source and target language.");
}

// Tests the behavior of failing to load the library as a result of the
// incompatibility of the library.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CreateTranslatorErrorLibraryIncompatible) {
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(1);
  mock_component_manager.InstallMockInvalidFunctionPointerLibraryComponent();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});
  NavigateToEmptyPage();

  auto console_observer =
      CreateConsoleObserver("The translation library is not compatible.");

  EXPECT_EQ(EvalJsCatchingError(R"(
            const translator = await Translator.create({
              sourceLanguage: 'en',
              targetLanguage: 'ja',
            });
      )"),
            "NotSupportedError: Unable to create translator for the given "
            "source and target language.");

  // The console message should be logged.
  WaitForConsoleObserver(*console_observer);
}

// Tests the behavior of failing to load the library as a result of the
// initialization failure of the library.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CreateTranslatorErrorLibraryFailedToInitialize) {
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(1);
  mock_component_manager.InstallMockFailingLibraryComponent();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});
  NavigateToEmptyPage();

  auto console_observer =
      CreateConsoleObserver("Failed to initialize the translation library.");

  EXPECT_EQ(EvalJsCatchingError(R"(
            const translator = await Translator.create({
              sourceLanguage: 'en',
              targetLanguage: 'ja',
            });
      )"),
            "NotSupportedError: Unable to create translator for the given "
            "source and target language.");

  // The console message should be logged.
  WaitForConsoleObserver(*console_observer);
}

// Tests the behavior of failure of translator creation in the library.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CreateTranslatorErrorLibraryTranslatorCreationFailed) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});
  NavigateToEmptyPage();

  auto console_observer = CreateConsoleObserver(
      "The translation library failed to create a translator.");

  EXPECT_EQ(EvalJsCatchingError(R"(
            const translator = await Translator.create({
              sourceLanguage: 'ja',
              targetLanguage: 'en',
            });
      )"),
            "NotSupportedError: Unable to create translator for the given "
            "source and target language.");

  // The console message should be logged.
  WaitForConsoleObserver(*console_observer);
}

// Tests the behavior of streaming translation.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest, TranslateStreaming) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});
  NavigateToEmptyPage();

  // Create a translator and call translateStreaming().
  // The mock sentence splitter will split the input by ".".
  // The mock translator will prepend "en to ja: " to each sentence.
  // The streaming response will be concatenated with spaces in between.
  // Confirms correctness of chunk content and the number of chunks streamed.
  EXPECT_EQ(EvalJsCatchingError(R"(
      const translator = await Translator.create({
          sourceLanguage: 'en',
          targetLanguage: 'ja',
        });
      const stream =
          translator.translateStreaming('Sentence one. Sentence two.');
      let result = '';
      const chunks = [];
      for await (const chunk of stream) {
        result += chunk;
        chunks.push(chunk);
      }
      if (chunks.length !== 2) return `Expected 2 chunks, got ${chunks.length}`;
      if (chunks[0] !== 'en to ja: Sentence one')
        return `Unexpected chunk 0: ${chunks[0]}`;
      if (chunks[1] !== ' en to ja: Sentence two')
        return `Unexpected chunk 1: ${chunks[1]}`;
      return result;
  )"),
            "en to ja: Sentence one en to ja: Sentence two");
}

// Tests the FIFO order of multiple parallel TranslateStreaming() invocations.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest, MultipleStreamingCalls) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});
  NavigateToEmptyPage();

  // Create one translator and call translateStreaming() multiple times in
  // parallel. The results should be correct and not interfere with each other.
  EXPECT_EQ(EvalJsCatchingError(R"(
      const translator = await Translator.create({
          sourceLanguage: 'en',
          targetLanguage: 'ja',
        });

      async function streamAndCollect(input) {
        const stream = translator.translateStreaming(input);
        let result = '';
        for await (const chunk of stream) {
          result += chunk;
        }
        return result;
      }

      const results = await Promise.all([
        streamAndCollect('First call. Sentence one.'),
        streamAndCollect('Second call. Sentence two.'),
      ]);

      return results.join('|');
  )"),
            "en to ja: First call en to ja: Sentence one|en to ja: Second call "
            "en to ja: Sentence two");
}

// Tests progress monitor behavior.
class OnDeviceTranslationProgressMonitorBrowserTest
    : public OnDeviceTranslationBrowserTest {
 public:
  OnDeviceTranslationProgressMonitorBrowserTest() = default;
  ~OnDeviceTranslationProgressMonitorBrowserTest() override = default;

  void SetUpOnMainThread() override {
    OnDeviceTranslationBrowserTest::SetUpOnMainThread();
    NavigateToEmptyPage();
    translation_manager_ = std::make_unique<MockTranslationManagerImpl>(
        GetRenderProcessHost(), GetBrowserContext(), GetLastCommittedOrigin(),
        &component_update_service_);

    // `GetComponentDetails` should be called by the
    // `OnDeviceModelDownloadProgressManager` to filter out existing
    // downloads.
    EXPECT_CALL(component_update_service_, GetComponentDetails(_, _))
        .WillRepeatedly(
            [&](const std::string& id, component_updater::CrxUpdateItem* item) {
              // The `total_bytes` doesn't matter since
              // `OnDeviceModelDownloadProgressManager` doesn't check it for
              // now.
              *item = GetComponentForTranslateKit(100).CreateUpdateItem(
                  update_client::ComponentState::kNew, 0);
              return true;
            });
  }

  void TearDownOnMainThread() override {
    translation_manager_.reset();
    OnDeviceTranslationBrowserTest::TearDownOnMainThread();
  }

  void TranslateAndMonitorProgress(std::string& source_language,
                                   std::string& target_language) {
    std::set<LanguagePackKey> language_pack_keys =
        CalculateRequiredLanguagePacks(source_language, target_language);

    base::RunLoop run_loop_translate_kit;
    EXPECT_CALL(component_manager_, RegisterTranslateKitComponentImpl())
        .WillOnce([&]() { run_loop_translate_kit.Quit(); });

    base::RunLoop run_loop_language_pack;
    EXPECT_CALL(component_manager_,
                RegisterTranslateKitLanguagePackComponent(_))
        .WillRepeatedly([&](LanguagePackKey key) {
          EXPECT_EQ(language_pack_keys.erase(key), 1u);
          if (language_pack_keys.empty()) {
            run_loop_language_pack.Quit();
          }
        });

    EXPECT_EQ(EvalJsCatchingError(base::StringPrintf(R"(
                  self.progressEvents = [];

                  self.createTranslatorPromise = Translator.create({
                    sourceLanguage: '%s',
                    targetLanguage: '%s',
                    monitor(m) {
                      m.addEventListener(
                          'downloadprogress', ({loaded, total}) => {
                            self.progressEvents.push({loaded, total});
                          });
                    },
                  });
                  return "OK";
                )",
                                                     source_language,
                                                     target_language)),
              "OK");

    run_loop_translate_kit.Run();
    run_loop_language_pack.Run();
  }

  FakeComponent GetComponentForTranslateKit(uint64_t total_bytes) {
    return {component_updater::TranslateKitComponentInstallerPolicy::
                GetExtensionId(),
            total_bytes};
  }

  FakeComponent GetComponentForLanguagePack(LanguagePackKey language_pack_key,
                                            uint64_t total_bytes) {
    const LanguagePackComponentConfig& config =
        GetLanguagePackComponentConfig(language_pack_key);
    std::string id =
        crx_file::id_util::GenerateIdFromHash(config.public_key_sha);
    return {id, total_bytes};
  }

  void SendUpdate(FakeComponent component, uint64_t downloaded_bytes) {
    component_update_service_.SendUpdate(component.CreateUpdateItem(
        update_client::ComponentState::kDownloading, downloaded_bytes));
  }

  double NormalizedProgress(uint64_t downloaded_bytes, uint64_t total_bytes) {
    // `optimization_guide::NormalizeModelDownloadProgress` normalizes to 0 -
    // 0x10000 range. We divide it by 0x10000 (65536) again to get it in the 0.0
    // - 1.0 range.
    return optimization_guide::NormalizeModelDownloadProgress(downloaded_bytes,
                                                              total_bytes) /
           65536.0;
  }

  void FinishInstalling(std::string& source_language,
                        std::string& target_language) {
    component_manager_.InstallMockTranslateKitComponentLater();

    std::set<LanguagePackKey> language_pack_keys =
        CalculateRequiredLanguagePacks(source_language, target_language);
    for (LanguagePackKey language_pack_key : language_pack_keys) {
      component_manager_.InstallMockLanguagePackLater(language_pack_key);
    }
  }

  void ExpectUpdatesAre(const std::vector<double>& expected_updates) {
    base::ListValue actual_updates = EvalJs(R"((async () => {
                            await self.createTranslatorPromise;
                            return self.progressEvents;
                          })())")
                                         .TakeValue()
                                         .TakeList();

    ASSERT_EQ(actual_updates.size(), expected_updates.size());
    for (size_t i = 0; i < actual_updates.size(); i++) {
      auto& actual_update = actual_updates[i].GetDict();
      std::optional<double> actual_loaded = actual_update.FindDouble("loaded");
      std::optional<double> actual_total = actual_update.FindDouble("total");
      ASSERT_TRUE(actual_loaded);
      ASSERT_TRUE(actual_total);

      double expected_loaded = expected_updates[i];
      EXPECT_EQ(*actual_loaded, expected_loaded);
      EXPECT_EQ(*actual_total, 1);
    }
  }

 private:
  MockComponentManager component_manager_{GetTempDir()};
  FakeComponentUpdateService component_update_service_;
  std::unique_ptr<MockTranslationManagerImpl> translation_manager_;

  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests that progress events are received properly when translation requires
// one language pack.
//
// TODO(crbug.com/403592445): Add another test for when translation requires two
// language packs. It's not possible currently since the browser tests can't
// translate between two non-english languages.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationProgressMonitorBrowserTest,
                       ReceivesProgressEventsForOneLanguagePack) {
  std::string source_language = "en";
  std::string target_language = "ja";
  TranslateAndMonitorProgress(source_language, target_language);

  // Components we expect to receive updates for.
  FakeComponent translation_kit = GetComponentForTranslateKit(4321);
  FakeComponent en_ja_language_pack =
      GetComponentForLanguagePack(LanguagePackKey::kEn_Ja, 1234);

  // The downloaded bytes and total bytes for all components.
  uint64_t downloaded_bytes = 0;
  uint64_t total_bytes =
      translation_kit.total_bytes() + en_ja_language_pack.total_bytes();

  std::vector<double> expected_updates = {};

  // Receives the zero update.
  {
    expected_updates.emplace_back(0);

    SendUpdate(translation_kit, 0);
    SendUpdate(en_ja_language_pack, 0);
  }

  // Receives an update for translation kit normalized to the total_bytes.
  {
    Sleep(base::Milliseconds(100));

    uint64_t update_bytes = 999;
    downloaded_bytes += update_bytes;
    SendUpdate(translation_kit, update_bytes);

    expected_updates.emplace_back(
        NormalizedProgress(downloaded_bytes, total_bytes));
  }

  // Receives an update for the en ja language pack normalized to the
  // total_bytes.
  {
    Sleep(base::Milliseconds(100));

    uint64_t update_bytes = 300;
    downloaded_bytes += update_bytes;
    SendUpdate(en_ja_language_pack, update_bytes);

    expected_updates.emplace_back(
        NormalizedProgress(downloaded_bytes, total_bytes));
  }

  // Receives the final one update when all bytes are loaded.
  {
    SendUpdate(translation_kit, translation_kit.total_bytes());
    SendUpdate(en_ja_language_pack, en_ja_language_pack.total_bytes());

    expected_updates.emplace_back(1);
  }

  FinishInstalling(source_language, target_language);

  ExpectUpdatesAre(expected_updates);
}

// Confirms that `Translator.availability()` is not masked for a translation
// containing only English or the user's preferred languages.
IN_PROC_BROWSER_TEST_F(
    OnDeviceTranslationBrowserTest,
    TranslatorAvailabilityNotMasked_EnglishAndPreferredLanguages) {
  SetSelectedLanguages("fr");
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockTranslateKitComponent();
  NavigateToEmptyPage();

  // The language pack for the preferred language is installed already.
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Fr);

  // When the required language packs are installed for translation
  // between English and a preferred language, `availability()` is not masked
  // prior to initial translator creation.
  TestTranslationAvailable(browser(), "en", "fr", "available");
}

// Tests that `Translator.availability()` for a translation
// containing a language outside of English + the user's preferred languages.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       TranslatorAvailabilityMasked_ForNonPreferredLanguages) {
  SetSelectedLanguages("fr");
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockTranslateKitComponent();
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Fr);
  NavigateToEmptyPage();

  // Translation is not available for unsupported languages.
  TestTranslationAvailable(browser(), "fr", "abcxyz", "unavailable");

  // The Japanese language pack needs to be downloaded in order for
  // translation between English and Japanese to become "available".
  TestTranslationAvailable(browser(), "en", "ja", "downloadable");

  // After installing the en-ja language pack, `availability()` is still masked
  // as "downloadable", because Japanese is not one of the preferred languages,
  // and the site has not yet initialized a translator for that specific pair.
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);
  TestTranslationAvailable(browser(), "en", "ja", "downloadable");

  // `availability()` is no longer masked as "downloadable" after creating an
  // initial translator for the given translation.
  TestSimpleTranslationWorks(browser(), "en", "ja");
  TestTranslationAvailable(browser(), "en", "ja", "available");

  // When site data is cleared (the stored values for
  // `ContentSettingsType::INTIALIZED_TRANSLATIONS` are cleared), the
  // translation `availability()` becomes masked once again.
  ClearSiteContentSettings();
  TestTranslationAvailable(browser(), "en", "ja", "downloadable");
}

// `Translator.create` should still require user activation if the language pair
// is readily available but the site hasn't created a Translator for the
// language pair yet.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CreateRequiresUserActivationWhenDownloadedButMasked) {
  SetSelectedLanguages("es");
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockTranslateKitComponent();
  NavigateToEmptyPage();

  // Simulate the download of an additional language pack (Japanese) by another
  // site.
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);

  ASSERT_EQ(
      EvalJsCatchingError(R"(
    await Translator.create({sourceLanguage: 'en', targetLanguage: 'ja'});
    return 'OK';
  )",
                          browser(), content::EXECUTE_SCRIPT_NO_USER_GESTURE),
      "NotAllowedError: Requires a user gesture when availability is "
      "\"downloading\" or \"downloadable\".");
}

// Tests the behavior of the crash of calling create() and availability().
class OnDeviceTranslationCrashingLangBrowserTest
    : public OnDeviceTranslationBrowserTest {
 public:
  OnDeviceTranslationCrashingLangBrowserTest() {
    // Need to set TranslationAPIAcceptLanguagesCheck to false to use a fake
    // language code `crash` to trigger a crash.
    scoped_feature_list_.InitWithFeaturesAndParameters(
        {{blink::features::kTranslationAPI,
          {{"TranslationAPIAcceptLanguagesCheck", "false"}}}},
        {});
  }
  ~OnDeviceTranslationCrashingLangBrowserTest() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tests the behavior of the crash of calling create().
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationCrashingLangBrowserTest,
                       CrashWhileCallingCreateTranslator) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.DoNotExpectCallRegisterTranslateKitComponent();
  mock_component_manager.DoNotExpectCallRegisterLanguagePackComponent();
  NavigateToEmptyPage();

  MockTranslationManagerImpl manager(
      GetRenderProcessHost(), GetBrowserContext(), GetLastCommittedOrigin(),
      g_browser_process->component_updater());
  manager.SetCrashesAllowed(true);

  // TODO(crbug.com/489429610): make the mock lib crash.
  auto console_observer =
      CreateConsoleObserver("The language pair is unsupported.");

  // Tries to create a translator for the fake language code `crash`. This
  // causes a crash in the mock TranslateKit component. See comments in
  // mock_translate_kit_lib.cc.
  EXPECT_EQ(EvalJsCatchingError(R"(
            const translator = await Translator.create({
              sourceLanguage: 'crash',
              targetLanguage: 'ja',
            });
      )"),
            "NotSupportedError: Unable to create translator for the given "
            "source and target language.");

  // The console message should be logged.
  WaitForConsoleObserver(*console_observer);
}

// Tests the behavior of the crash of calling availability().
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationCrashingLangBrowserTest,
                       CrashWhileCallingCanTranslate) {
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(0);
  mock_component_manager.InstallMockTranslateKitComponent();
  NavigateToEmptyPage();

  MockTranslationManagerImpl manager(
      GetRenderProcessHost(), GetBrowserContext(), GetLastCommittedOrigin(),
      g_browser_process->component_updater());
  manager.SetCrashesAllowed(true);

  // Tries to call availability() for the fake language code `crash`. This
  // causes a crash in the mock TranslateKit component. See comments in
  // mock_translate_kit_lib.cc.
  // TODO(crbug.com/489429610): make the mock lib crash.
  TestCanTranslateResult("crash", "ja",
                         CanCreateTranslatorResult::kNoNotSupportedLanguage);
}

IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest, NoExistFileHandling) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});

  NavigateToEmptyPage();

  EXPECT_EQ(EvalJsCatchingError(R"(
      const translator = await Translator.create({
        sourceLanguage: 'en',
        targetLanguage: 'ja',
      });
      return await translator.translate('CHECK_NOT_EXIST_FILE');
  )"),
            "Result of Open(): Failed, result of FileExists(): false, "
            "is_directory: false");
}

// Tests the behavior of create() when the accept language check
// fails.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CreateTranslatorAcceptLanguagesCheckFailed) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.DoNotExpectCallRegisterTranslateKitComponent();
  mock_component_manager.DoNotExpectCallRegisterLanguagePackComponent();

  NavigateToEmptyPage();

  auto console_observer =
      CreateConsoleObserver("The language pair is unsupported.");

  // Create a translator for unsupported language.
  TestCreateTranslator(browser(), "en", "xx",
                       "NotSupportedError: Unable to create translator for the "
                       "given source and target language.");

  // The console message should be logged.
  WaitForConsoleObserver(*console_observer);
}

// Tests that the browser process can handle the case that the frame is deleted
// while creating a translator.
// Flaky. See crbug.com/484300734
#if defined(MEMORY_SANITIZER) || defined(UNDEFINED_SANITIZER)
#define MAYBE_FrameDeletedWhileCreatingATranslator \
  DISABLED_FrameDeletedWhileCreatingATranslator
#else
#define MAYBE_FrameDeletedWhileCreatingATranslator \
  FrameDeletedWhileCreatingATranslator
#endif
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       MAYBE_FrameDeletedWhileCreatingATranslator) {
  MockComponentManager mock_component_manager(GetTempDir());

  base::RunLoop run_loop_for_register_translate_kit;
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .WillOnce([&]() { run_loop_for_register_translate_kit.Quit(); });

  base::RunLoop run_loop_for_register_language_pack;
  EXPECT_CALL(mock_component_manager,
              RegisterTranslateKitLanguagePackComponent(_))
      .WillOnce([&](LanguagePackKey key) {
        EXPECT_EQ(key, LanguagePackKey::kEn_Ja);
        run_loop_for_register_language_pack.Quit();
      });

  NavigateToEmptyPage();

  content::RenderFrameHost* iframe = CreateIframe();

  // Create a translator in an iframe.
  EXPECT_EQ(content::EvalJs(iframe, R"(
      Translator.create({
          sourceLanguage: 'en',
          targetLanguage: 'ja',
        });
      'OK';
    )")
                .ExtractString(),
            "OK");
  // Wait until RegisterTranslateKitComponentImpl() is called.
  run_loop_for_register_translate_kit.Run();
  // Wait until RegisterTranslateKitLanguagePackComponent() is called.
  run_loop_for_register_language_pack.Run();

  // Deletes the iframe after the browser process receives the request.
  EXPECT_TRUE(RemoveIframe());

  // Install the mock TranslateKit component.
  mock_component_manager.InstallMockTranslateKitComponent();
  // Install the mock language pack.
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);

  // Test that Translator API works even after the frame requesting a translator
  // is deleted.
  TestSimpleTranslationWorks(browser(), "en", "ja");
}

// Tests that the service is terminated when the idle timeout is reached.
IN_PROC_BROWSER_TEST_F(
    OnDeviceTranslationBrowserTest,
    TerminateServiceWhenIdleTimeoutReachedAfterTranslatorDestroyed) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});

  NavigateToEmptyPage();

  url::Origin origin = embedded_https_test_server().GetOrigin();
  auto* manager =
      ServiceControllerManagerFactory::GetInstance()->Get(browser()->GetProfile());
  manager->SetServiceIdleTimeoutForTesting(origin, base::Microseconds(100));

  // Test that Translator API works.
  EXPECT_EQ(EvalJsCatchingError(R"(
      window._translator = await Translator.create({
        sourceLanguage: 'en',
        targetLanguage: 'ja',
      });
      return await window._translator.translate('hello');
    )"),
            "en to ja: hello");
  // Check that the service is still running.
  EXPECT_TRUE(manager->IsServiceRunning(origin));
  // Wait for 200 microseconds.
  EXPECT_EQ(EvalJsCatchingError(R"(
      await new Promise(resolve => { setTimeout(resolve, 200); });
      return 'OK';
    )"),
            "OK");
  // Check that the service is still running, because the translator is still
  // available.
  EXPECT_TRUE(manager->IsServiceRunning(origin));
  // Destroy the translator. And wait for 200 microseconds. (Note: wait more
  // than the idle timeout 100 microseconds to avoid flakiness.)
  EXPECT_EQ(EvalJsCatchingError(R"(
      window._translator.destroy();
      await new Promise(resolve => { setTimeout(resolve, 200); });
      return 'OK';
    )"),
            "OK");
  // Check that the service is not running, because the translator was
  // destroyed, and the idle timeout was reached.
  EXPECT_FALSE(manager->IsServiceRunning(origin));
}

// Tests that the service is terminated when the idle timeout is reached after
// the frame is removed.
IN_PROC_BROWSER_TEST_F(
    OnDeviceTranslationBrowserTest,
    TerminateServiceWhenIdleTimeoutReachedAfterFrameRemoved) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});

  NavigateToEmptyPage();
  url::Origin origin = embedded_https_test_server().GetOrigin();

  auto* manager =
      ServiceControllerManagerFactory::GetInstance()->Get(browser()->GetProfile());
  manager->SetServiceIdleTimeoutForTesting(origin, base::Microseconds(100));
  content::RenderFrameHost* iframe = CreateIframe();

  // Test that Translator API on an iframe works.
  EXPECT_EQ(content::EvalJs(iframe, R"(
     (async () => {
        const translator =
            await Translator.create({
              sourceLanguage: 'en',
              targetLanguage: 'ja',
            });
        return await translator.translate('hello');
      })();
    )"),
            "en to ja: hello");
  // Check that the service is still running.
  EXPECT_TRUE(manager->IsServiceRunning(origin));
  // Wait for 200 microseconds.
  EXPECT_EQ(EvalJsCatchingError(R"(
      await new Promise(resolve => { setTimeout(resolve, 200); });
      return 'OK';
    )"),
            "OK");
  // Check that the service is still running, because the ifame is still
  // available.
  EXPECT_TRUE(manager->IsServiceRunning(origin));
  // Remove the iframe and wait for 200 microseconds. (Note: wait more than the
  // idle timeout 100 microseconds to avoid flakiness.)
  EXPECT_EQ(EvalJsCatchingError(R"(
      document.body.removeChild(window._iframe);
      await new Promise(resolve => { setTimeout(resolve, 200); });
      return 'OK';
    )"),
            "OK");
  // Check that the service is not running, because the iframe was removed, and
  // the idle timeout was reached.
  EXPECT_FALSE(manager->IsServiceRunning(origin));
}

// Test the behavior of availability() when the language pack is ready.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest, CanTranslateReadily) {
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(0);
  mock_component_manager.InstallMockTranslateKitComponent();
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);

  // Despite being ready, the availability will be masked since the site hasn't
  // created a translator for this language pair yet.
  // `kAfterDownloadTranslatorCreationRequired` is only ever returned in that
  // situation, so receiving that value confirms that the package is readily
  // available.
  TestCanTranslateResult(
      "en", "ja",
      CanCreateTranslatorResult::kAfterDownloadTranslatorCreationRequired);
}

// Test the behavior of availability() when the language pack is not ready.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CanTranslateAfterDownloadLanguagePackNotReady) {
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(0);
  mock_component_manager.InstallMockTranslateKitComponent();
  TestCanTranslateResult(
      "en", "ja",
      CanCreateTranslatorResult::kAfterDownloadLanguagePackNotReady);
}

// Test the behavior of availability() when both the library and the language
// pack are not ready.
IN_PROC_BROWSER_TEST_F(
    OnDeviceTranslationBrowserTest,
    CanTranslateAfterDownloadLibraryAndLanguagePackNotReady) {
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(0);
  TestCanTranslateResult(
      "en", "ja",
      CanCreateTranslatorResult::kAfterDownloadLibraryAndLanguagePackNotReady);
}

// Test the behavior of availability() when the language pack is ready, but the
// library is not ready.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CanTranslateAfterDownloadLibraryNotReady) {
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(0);
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);
  TestCanTranslateResult(
      "en", "ja", CanCreateTranslatorResult::kAfterDownloadLibraryNotReady);
}

// CreateGuestBrowser isn't available on ChromeOS
#if !BUILDFLAG(IS_CHROMEOS)
// Tests the behavior when the Translator API is accessed from an invalid
// storage partition.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CanTranslateInvalidStoragePartition) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockTranslateKitComponent();
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);

  // Create a guest browser profile in order to access a non-default storage
  // partition, and navigate to an empty page.
  Browser* guest_browser = CreateGuestBrowser();
  ASSERT_TRUE(guest_browser);

  guest_browser->GetProfile()->GetPrefs()->SetString(
      language::prefs::kSelectedLanguages, "ja");

  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      guest_browser, embedded_https_test_server().GetURL("/empty.html")));

  mojo::Remote<blink::mojom::TranslationManager> remote;
  TestSupportsUserData fake_user_data;

  content::RenderProcessHost* process_host = guest_browser->tab_strip_model()
                                                 ->GetActiveWebContents()
                                                 ->GetPrimaryMainFrame()
                                                 ->GetProcess();
  const url::Origin last_committed_origin = guest_browser->tab_strip_model()
                                                ->GetActiveWebContents()
                                                ->GetPrimaryMainFrame()
                                                ->GetLastCommittedOrigin();

  TranslationManagerImpl::Bind(process_host, GetBrowserContext(),
                               &fake_user_data, last_committed_origin,
                               g_browser_process->component_updater(),
                               remote.BindNewPipeAndPassReceiver());

  // Check the availability result.
  base::RunLoop run_loop;
  remote->TranslationAvailable(
      TranslatorLanguageCode::New(std::string("en")),
      TranslatorLanguageCode::New(std::string("ja")),
      base::BindLambdaForTesting([&](CanCreateTranslatorResult result) {
        EXPECT_EQ(result,
                  CanCreateTranslatorResult::kNoInvalidStoragePartition);
        run_loop.Quit();
      }));
  run_loop.Run();
}
#endif  // !BUILDFLAG(IS_CHROMEOS)

// Test the behavior of availability() when the language is not supported.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       CanTranslateNoNotSupportedLanguage) {
  // This test case uses English as the source language and an unsupported
  // language code as the target language. To avoid the failure of
  // PassAcceptLanguagesCheck(), we set the SelectedLanguages to be English and
  // the unsupported language code.
  SetSelectedLanguages("en,xx");
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(0);
  TestCanTranslateResult("en", "xx",
                         CanCreateTranslatorResult::kNoNotSupportedLanguage);
}

// Test the behavior of `availability()` when the execution context is not
// valid.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       Availability_InvalidStateError) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockTranslateKitComponent();
  NavigateToEmptyPage();
  EXPECT_EQ(EvalJsCatchingError(R"(
        const iframe = document.createElement('iframe');
        const loadPromise = new Promise(resolve => {
          iframe.addEventListener('load', resolve);
        });
        iframe.src = location.href;
        document.body.appendChild(iframe);
        await loadPromise;
        const iframeTranslator = iframe.contentWindow.Translator;
        document.body.removeChild(iframe);
        await iframeTranslator.availability({
          sourceLanguage: "en",
          targetLanguage: "es"
        });
      )"),
            "InvalidStateError: Failed to execute 'availability' on "
            "'Translator': The execution context is not valid.");
}

// Test the behavior of `availability()` for an unsupported language.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       Availability_Unavailable_NotSupportedLanguage) {
  // Set the unsupported language as a preferred language in order to avoid
  // `PassAcceptLanguagesCheck()` failure, for testing purposes.
  SetSelectedLanguages("en,xx");
  MockComponentManager mock_component_manager(GetTempDir());
  NavigateToEmptyPage();
  TestTranslationAvailable(browser(), "en", "xx", "unavailable");
}

// Test the behavior of `availability()` where the source language and the
// target language are the same language.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       Availability_Unavailable_SameSourceAndTargetLanguage) {
  SetSelectedLanguages("en,ja");
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);
  NavigateToEmptyPage();
  TestTranslationAvailable(browser(), "ja", "ja", "unavailable");
  TestTranslationAvailable(browser(), "en", "en", "unavailable");
}

// Test the behavior of `availability()` when both the library and the language
// packs are not ready.
IN_PROC_BROWSER_TEST_F(
    OnDeviceTranslationBrowserTest,
    Availability_Downloadable_LibraryAndLanguagePackNotReady) {
  SetSelectedLanguages("en,ja");
  MockComponentManager mock_component_manager(GetTempDir());
  NavigateToEmptyPage();
  TestTranslationAvailable(browser(), "en", "ja", "downloadable");
}

// Test the behavior of `availability()` when the library is not ready.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       Availability_Downloadable_LibraryNotReady) {
  SetSelectedLanguages("en,es");
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(0);
  NavigateToEmptyPage();
  TestTranslationAvailable(browser(), "en", "es", "downloadable");
}

// Test the behavior of `availability()` when the library is ready, and
// language packs are not ready.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       Availability_Downloadable_LanguagePacksNotReady) {
  SetSelectedLanguages("en,es");
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockTranslateKitComponent();
  NavigateToEmptyPage();
  TestTranslationAvailable(browser(), "en", "es", "downloadable");
}

IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       Availability_Available_ForSelectedPopularAndNonPopular) {
  SetSelectedLanguages("ja,fr");
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockTranslateKitComponent();
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Fr);
  NavigateToEmptyPage();

  TestTranslationAvailable(browser(), "ja", "fr", "available");
}

// Test the behavior of `availability()` when both the library and language
// packs are ready.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       Availability_Available_LibraryAndLanguagePackReady) {
  SetSelectedLanguages("en,ja");
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockTranslateKitComponent();
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);
  NavigateToEmptyPage();
  TestTranslationAvailable(browser(), "en", "ja", "available");
}

// Test that calling both the legacy and new API works.
// This is a regression test for https://crbug.com/381344025.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest, UseBothLegacyAndNewAPI) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});

  NavigateToEmptyPage();

  // Test that Translator legacy API works.
  EXPECT_EQ(EvalJsCatchingError(R"(
      const translator = await Translator.create({
        sourceLanguage: 'en',
        targetLanguage: 'ja',
      });
      return await translator.translate('hello');
    )"),
            "en to ja: hello");

  // Test that Translator new API works.
  EXPECT_EQ(EvalJsCatchingError(R"(
      const translator = await Translator.create({
        sourceLanguage: 'en',
        targetLanguage: 'ja',
      });
      return await translator.translate('hello');
    )"),
            "en to ja: hello");
}

// TODO(crbug.com/410842873): Add test to check behavior for extension
// service workers once supported.
//
// Test the behavior of the Translator API accessed from a service worker
// outside of an extension.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest,
                       APIAvailability_NonExtensionWorkers) {
  const std::string kWorkerScript =
      "try {"
      "    Translator;"
      "    self.postMessage('test');"
      "} catch (e) {"
      "    self.postMessage(e.name);"
      "}";

  base::RunLoop loop;
  embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
      &RespondWithJS, "/js-response", kWorkerScript, loop.QuitClosure()));
  ASSERT_TRUE(embedded_test_server()->Start());
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(),
      embedded_test_server()->GetURL(
          "/workers/create_dedicated_worker.html?worker_url=/js-response")));
  loop.Run();

  EXPECT_EQ(
      "ReferenceError",
      content::EvalJs(browser()->tab_strip_model()->GetActiveWebContents(),
                      "waitForMessage();"));
}

// Test the behavior of availability() when PassAcceptLanguagesCheck() checks
// is skipped.
class OnDeviceTranslationSkipAcceptLanguagesCheckBrowserTest
    : public OnDeviceTranslationBrowserTest {
 public:
  OnDeviceTranslationSkipAcceptLanguagesCheckBrowserTest() {
    scoped_feature_list_.InitWithFeaturesAndParameters(
        {{blink::features::kTranslationAPI,
          {{"TranslationAPIAcceptLanguagesCheck", "false"}}}},
        {});
  }
  ~OnDeviceTranslationSkipAcceptLanguagesCheckBrowserTest() override = default;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Test the behavior of availability() when PassAcceptLanguagesCheck() checks
// is skipped.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationSkipAcceptLanguagesCheckBrowserTest,
                       CanTranslateAcceptLanguagesCheckSkipped) {
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(0);
  mock_component_manager.InstallMockTranslateKitComponent();
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ko);
  // Despite being ready, the availability will be masked since the site hasn't
  // created a translator for this language pair yet.
  // `kAfterDownloadTranslatorCreationRequired` is only ever returned in that
  // situation, so receiving that value confirms that the package is readily
  // available.
  TestCanTranslateResult(
      "en", "ko",
      CanCreateTranslatorResult::kAfterDownloadTranslatorCreationRequired);
}

// Test the behavior of Translator API in a cross origin iframe.
class OnDeviceTranslationCrossOriginBrowserTest
    : public OnDeviceTranslationBrowserTest {
 public:
  OnDeviceTranslationCrossOriginBrowserTest() {
    // Use a reduced value for TranslationAPIMaxServiceCount to speed
    // up the test.
    scoped_feature_list_.InitWithFeaturesAndParameters(
        {{blink::features::kTranslationAPI,
          {{"TranslationAPIMaxServiceCount", "2"}}}},
        {});
    CHECK_EQ(kTranslationAPIMaxServiceCount.Get(), 2u);
  }
  ~OnDeviceTranslationCrossOriginBrowserTest() override = default;

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    // Need to use URLLoaderInterceptor to handle requests to multiple origins.
    url_loader_interceptor_.emplace(base::BindRepeating(
        &OnDeviceTranslationCrossOriginBrowserTest::InterceptRequest));
  }

  void TearDownOnMainThread() override { url_loader_interceptor_.reset(); }

 protected:
  // Creates a URL for an iframe with a unique origin.
  static GURL CreateCrossOriginIframeUrl(size_t index) {
    return GURL(
        base::StringPrintf("https://test-%zd.example/frame.html", index));
  }

  // Navigates to the test page.
  void NavigateToTestPage(Browser* target_browser) {
    CHECK(ui_test_utils::NavigateToURL(
        target_browser ? target_browser : browser(),
        GURL("https://translation-api.test/index.html")));
  }

  // Adds an iframe to the test page and optionally sets its permission policy.
  content::RenderFrameHost* AddIframe(size_t index,
                                      Browser* target_browser,
                                      bool permission_policy_enabled) {
    EXPECT_EQ(EvalJsCatchingError(JsReplace("return addIframe($1, $2);",
                                            CreateCrossOriginIframeUrl(index),
                                            permission_policy_enabled),
                                  target_browser),
              "loaded");

    return ChildFrameAt((target_browser ? target_browser : browser())
                            ->tab_strip_model()
                            ->GetActiveWebContents(),
                        index);
  }

  // Removes the iframe and waits for the service deletion.
  void RemoveIframeAndWaitForServiceDeletion(size_t index,
                                             Browser* target_browser) {
    auto* manager = ServiceControllerManagerFactory::GetInstance()->Get(
        target_browser->GetProfile());
    url::Origin origin = url::Origin::Create(CreateCrossOriginIframeUrl(index));

    EXPECT_EQ(EvalJsCatchingError(JsReplace("return removeIframe($1);",
                                            CreateCrossOriginIframeUrl(index)),
                                  target_browser),
              "removed");
    EXPECT_TRUE(base::test::RunUntil(
        [&]() { return !manager->IsServiceRunning(origin); }));
  }

  // Creates a translator and translates in the iframe. Returns successful
  // translation or the error message.
  std::string CheckTranslateInIframe(content::RenderFrameHost* iframe) {
    const std::string_view translateTestScript = R"(
        (async () => {
          try {
            window._translator = await Translator.create({
              sourceLanguage: 'en',
              targetLanguage: 'ja',
            });
            if (window._translator) {
              return await window._translator.translate('hello');
            }
          } catch (e) {
            return e.name.toString();
          }
        })()
      )";
    return content::EvalJs(iframe, translateTestScript).ExtractString();
  }

  // Checks the result of availability() in the iframe.
  std::string TryCanTranslateInIframe(content::RenderFrameHost* iframe) {
    const std::string_view translateTestScript = R"(
      (async () => {
        try {
          return await Translator.availability({
            sourceLanguage: 'en',
            targetLanguage: 'ja',
          });
        } catch (e) {
          return e.name.toString();
        }
      })()
    )";
    return content::EvalJs(iframe, translateTestScript).ExtractString();
  }

 private:
  // URLLoaderInterceptor callback
  static bool InterceptRequest(
      content::URLLoaderInterceptor::RequestParams* params) {
    if (params->url_request.url.GetPath() == "/index.html") {
      content::URLLoaderInterceptor::WriteResponse(
          "HTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\n\n",
          R"(
          <head><script>
          const frames = {};
          async function addIframe(url, permissionPolicyEnabled) {
            const frame = document.createElement('iframe');
            frames[url] = frame;
            if (permissionPolicyEnabled) {
              frame.setAttribute('allow', 'translator');
            }
            const loadedPromise = new Promise(resolve => {
              frame.addEventListener('load', () => {
                resolve('loaded');
              });
            });
            frame.src = url;
            document.body.appendChild(frame);
            return await loadedPromise;
          }
          async function evalInIframe(url, script) {
            const frame = frames[url];
            const channel = new MessageChannel();
            const onMessagePromise = new Promise(resolve => {
              channel.port1.addEventListener('message', e => {
                resolve(e.data);
              });
            });
            channel.port1.start();
            frame.contentWindow.postMessage(script, '*', [channel.port2]);
            return await onMessagePromise;
          }
          function removeIframe(url) {
            document.body.removeChild(frames[url]);
            return 'removed';
          }
          </script></head>)",
          params->client.get(),
          /*ssl_info=*/std::nullopt, params->url_request.url);
      return true;
    } else if (params->url_request.url.GetPath() == "/frame.html") {
      content::URLLoaderInterceptor::WriteResponse(
          "HTTP/1.1 200 OK\nContent-Type: text/html; charset=utf-8\n\n",
          R"(
          <head><script>
          window.addEventListener('message', async (e) => {
            e.ports[0].postMessage(await eval(e.data));
          });
          </script></head>)",
          params->client.get(),
          /*ssl_info=*/std::nullopt, params->url_request.url);
      return true;
    }
    return false;
  }

  base::test::ScopedFeatureList scoped_feature_list_;
  std::optional<content::URLLoaderInterceptor> url_loader_interceptor_;

 protected:
  FakeOnDeviceTranslationInstaller fake_installer_{GetTempDir()};
};

// Tests the behavior of the Translation API in a cross origin iframe.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationCrossOriginBrowserTest,
                       TranslateInCrossOriginIframe) {
  MockComponentManager mock_component_manager(GetTempDir());
  EXPECT_CALL(mock_component_manager, RegisterTranslateKitComponentImpl())
      .Times(0);

  NavigateToTestPage(browser());
  content::RenderFrameHost* iframe =
      AddIframe(0, browser(), /*permission_policy_enabled=*/false);

  // Translation is not available in cross-origin iframes without permission
  // policy.
  EXPECT_EQ(CheckTranslateInIframe(iframe), "NotAllowedError");
}

// Tests the behavior of the Translation API in a cross origin iframe when the
// service count exceeds the limit.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationCrossOriginBrowserTest,
                       ExceedServiceCountLimit) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockTranslateKitComponent();
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);
  fake_installer_.InitNow(base::DoNothing());
  fake_installer_.InstallLanguagePackNow(LanguagePackKey::kEn_Ja);
  base::ScopedAllowBlockingForTesting allow_io;
  CHECK(base::CopyFile(GetMockLibraryPath(), fake_installer_.GetLibraryPath()));
  auto* manager =
      ServiceControllerManagerFactory::GetInstance()->Get(browser()->GetProfile());
  manager->SetInstallerForTesting(&fake_installer_);
  NavigateToTestPage(browser());
  size_t i = 0;
  // Until the service count exceeds the limit, the translator can be created,
  // and the translation is successful.
  for (; i < kTranslationAPIMaxServiceCount.Get(); i++) {
    content::RenderFrameHost* iframe =
        AddIframe(i, browser(), /*permission_policy_enabled=*/true);
    EXPECT_EQ(CheckTranslateInIframe(iframe), "en to ja: hello");
    EXPECT_EQ(TryCanTranslateInIframe(iframe), "available");
  }

  // When the service count exceeds the limit, the translator cannot be created,
  // even when the permission policy is still enabled.
  content::RenderFrameHost* iframe =
      AddIframe(i, browser(), /*permission_policy_enabled=*/true);
  auto console_observer = CreateConsoleObserver(
      "The translation service count exceeded the limitation.");
  EXPECT_EQ(CheckTranslateInIframe(iframe), "NotSupportedError");
  WaitForConsoleObserver(*console_observer);
  EXPECT_EQ(TryCanTranslateInIframe(iframe), "unavailable");

  // When the service count is back to under the limit, the translator can be
  // created again.
  RemoveIframeAndWaitForServiceDeletion(0, browser());
  EXPECT_EQ(CheckTranslateInIframe(iframe), "en to ja: hello");
  EXPECT_EQ(TryCanTranslateInIframe(iframe), "available");
}

// Tests the behavior of the Translation API in a cross origin iframe using the
// incognito profile.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationCrossOriginBrowserTest,
                       TranslateInIframeIncognitoBrowser) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockTranslateKitComponent();
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);
  fake_installer_.InitNow(base::DoNothing());
  fake_installer_.InstallLanguagePackNow(LanguagePackKey::kEn_Ja);
  base::ScopedAllowBlockingForTesting allow_io;
  CHECK(base::CopyFile(GetMockLibraryPath(), fake_installer_.GetLibraryPath()));

  Browser* incognito_browser = CreateIncognitoBrowser();
  auto* manager = ServiceControllerManagerFactory::GetInstance()->Get(
      incognito_browser->GetProfile());
  manager->SetInstallerForTesting(&fake_installer_);

  NavigateToTestPage(incognito_browser);
  content::RenderFrameHost* iframe0 =
      AddIframe(0, incognito_browser, /*permission_policy_enabled=*/true);
  EXPECT_EQ(CheckTranslateInIframe(iframe0), "en to ja: hello");

  content::RenderFrameHost* iframe1 =
      AddIframe(1, incognito_browser, /*permission_policy_enabled=*/false);
  EXPECT_EQ(CheckTranslateInIframe(iframe1), "NotAllowedError");
}

// CreateGuestBrowser isn't available on ChromeOS
#if !BUILDFLAG(IS_CHROMEOS)
// Tests the behavior of the Translation API in a cross origin iframe using the
// guest profile.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationCrossOriginBrowserTest,
                       TranslateInIframeGuestBrowser) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.InstallMockTranslateKitComponent();
  mock_component_manager.InstallMockLanguagePack(LanguagePackKey::kEn_Ja);
  fake_installer_.InitNow(base::DoNothing());
  fake_installer_.InstallLanguagePackNow(LanguagePackKey::kEn_Ja);
  base::ScopedAllowBlockingForTesting allow_io;
  CHECK(base::CopyFile(GetMockLibraryPath(), fake_installer_.GetLibraryPath()));

  Browser* guest_browser = CreateGuestBrowser();
  auto* manager = ServiceControllerManagerFactory::GetInstance()->Get(
      guest_browser->GetProfile());
  manager->SetInstallerForTesting(&fake_installer_);

  NavigateToTestPage(guest_browser);
  content::RenderFrameHost* iframe =
      AddIframe(0, guest_browser, /*permission_policy_enabled=*/true);
  EXPECT_EQ(CheckTranslateInIframe(iframe), "en to ja: hello");
}
#endif  // !BUILDFLAG(IS_CHROMEOS)

// CreateGuestBrowser isn't available on ChromeOS
#if !BUILDFLAG(IS_CHROMEOS)
// TODO(crbug.com/423029203): This is timing out on a CQ bot so it is disabled
// for now until we can resolve that issue.
//
// Tests the behavior of the Translation API in a cross origin iframe using
// multiple profiles.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationCrossOriginBrowserTest,
                       DISABLED_ServiceCountLimitIsolatedPerProfile) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});

  ProfileManager* profile_manager = g_browser_process->profile_manager();
  base::FilePath other_path =
      profile_manager->GenerateNextProfileDirectoryPath();
  // Create an additional profile.
  Profile& additional_profile =
      profiles::testing::CreateProfileSync(profile_manager, other_path);

  std::vector<Browser*> browsers = {
      browser(),
      CreateBrowser(&additional_profile),
      CreateIncognitoBrowser(),
      CreateGuestBrowser(),
  };

  for (auto* target_browser : browsers) {
    NavigateToTestPage(target_browser);
  }

  // Create translators and translate in each profile. Until the service count
  // per profile exceeds the limit, the translator can be created, and the
  // translation is successful.
  for (size_t i = 0; i < kTranslationAPIMaxServiceCount.Get(); i++) {
    for (auto* target_browser : browsers) {
      content::RenderFrameHost* iframe =
          AddIframe(i, target_browser, /*permission_policy_enabled=*/true);
      EXPECT_EQ(CheckTranslateInIframe(iframe), "en to ja: hello");
    }
  }

  const size_t limit_count = kTranslationAPIMaxServiceCount.Get();

  std::vector<content::RenderFrameHost*> iframes;

  // When the service count per profile exceeds the limit, the translator
  // cannot be created.
  for (auto* target_browser : browsers) {
    content::RenderFrameHost* iframe = AddIframe(
        limit_count, target_browser, /*permission_policy_enabled=*/true);
    iframes.push_back(iframe);
    auto console_observer = CreateConsoleObserver(
        "The translation service count exceeded the limitation.",
        target_browser);
    EXPECT_EQ(CheckTranslateInIframe(iframe), "NotSupportedError");
    // The console message should be logged.
    WaitForConsoleObserver(*console_observer);
  }

  ASSERT_EQ(iframes.size(), browsers.size());

  // When the service count per profile is back to under the limit, the
  // translator can be created again.
  for (size_t i = 0; i < browsers.size(); i++) {
    Browser* target_browser = browsers[i];
    content::RenderFrameHost* iframe = iframes[i];
    RemoveIframeAndWaitForServiceDeletion(0, target_browser);
    EXPECT_EQ(CheckTranslateInIframe(iframe), "en to ja: hello");
  }
}
#endif  // !BUILDFLAG(IS_CHROMEOS)

// Tests the behavior of when the command line flags "translate-kit-binary-path"
// and "translate-kit-packages" are provided.
class OnDeviceTranslationBinaryPathAndPackagesCommandLineBrowserTest
    : public OnDeviceTranslationBrowserTest {
 public:
  OnDeviceTranslationBinaryPathAndPackagesCommandLineBrowserTest() = default;
  ~OnDeviceTranslationBinaryPathAndPackagesCommandLineBrowserTest() override =
      default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    OnDeviceTranslationBrowserTest::SetUpCommandLine(command_line);
    SetMockLibraryPathToCommandLine(command_line);
    WriteFakeDictionaryDataAndSetCommandLine(LanguagePackKey::kEn_Ja,
                                             GetTempDir(), command_line);
  }
};

// Tests the behavior of when the number of values passed to the
// "translate-kit-packages" command-line flag is not a multiple of three.
class OnDeviceTranslationInvalidCommandLineNonTriadPackageFlagBrowserTest
    : public OnDeviceTranslationBrowserTest {
 public:
  OnDeviceTranslationInvalidCommandLineNonTriadPackageFlagBrowserTest() =
      default;
  ~OnDeviceTranslationInvalidCommandLineNonTriadPackageFlagBrowserTest()
      override = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    OnDeviceTranslationBrowserTest::SetUpCommandLine(command_line);
    // This is an invalid flag as "translate-kit-packages" requires
    // arguments in groups of three: "first language code, second language code,
    // model path".
    command_line->AppendSwitchASCII("translate-kit-packages", "en,ja");
  }
};
IN_PROC_BROWSER_TEST_F(
    OnDeviceTranslationInvalidCommandLineNonTriadPackageFlagBrowserTest,
    SimpleTranslation) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});
  NavigateToEmptyPage();
  TestSimpleTranslationWorks(browser(), "en", "ja");
}

// Tests the behavior of when non-ASCII language code is passed to the
// "translate-kit-packages" command-line flag.
class OnDeviceTranslationInvalidCommandLineNonAsciiLanguageFlagBrowserTest
    : public OnDeviceTranslationBrowserTest {
 public:
  OnDeviceTranslationInvalidCommandLineNonAsciiLanguageFlagBrowserTest() =
      default;
  ~OnDeviceTranslationInvalidCommandLineNonAsciiLanguageFlagBrowserTest()
      override = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    OnDeviceTranslationBrowserTest::SetUpCommandLine(command_line);
    // This is an invalid flag as "translate-kit-packages" requires
    // arguments in groups of three: "first language code, second language code,
    // model path", and the language codes must be ASCII.
    command_line->AppendSwitchASCII(
        "translate-kit-packages",
        base::StrCat({"en,日本語,", GetTempDir().AsUTF8Unsafe()}));
  }
};
IN_PROC_BROWSER_TEST_F(
    OnDeviceTranslationInvalidCommandLineNonAsciiLanguageFlagBrowserTest,
    SimpleTranslation) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});
  NavigateToEmptyPage();
  TestSimpleTranslationWorks(browser(), "en", "ja");
}

// Tests the behavior of the Translator API on a page with a file scheme URL.
IN_PROC_BROWSER_TEST_F(OnDeviceTranslationBrowserTest, FileSchemeUrl) {
  MockComponentManager mock_component_manager(GetTempDir());
  mock_component_manager.ExpectCallRegisterTranslateKitComponentAndInstall();
  mock_component_manager.ExpectCallRegisterLanguagePackComponentAndInstall(
      {LanguagePackKey::kEn_Ja});
  base::ScopedAllowBlockingForTesting allow_blocking;
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());
  base::FilePath empty_html_path = temp_dir.GetPath().AppendASCII("empty.html");
  ASSERT_TRUE(base::WriteFile(empty_html_path, "<html></html>"));
  base::FilePath empty2_html_path =
      temp_dir.GetPath().AppendASCII("empty2.html");
  ASSERT_TRUE(base::WriteFile(empty2_html_path, "<html></html>"));
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), net::FilePathToFileURL(empty_html_path)));

  // The language pack for Japanese needs to be downloaded.
  TestTranslationAvailable(browser(), "en", "ja", "downloadable");

  // Create a translator.
  EXPECT_EQ(EvalJsCatchingError(R"(
      window._translator = await Translator.create({
        sourceLanguage: 'en',
        targetLanguage: 'ja',
      });
      return await window._translator.translate('hello');
    )"),
            "en to ja: hello");

  // After creating a translator, the language pair should be available.
  TestTranslationAvailable(browser(), "en", "ja", "available");

  // Navigate to another file URL. The availability should be reset.
  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), net::FilePathToFileURL(empty2_html_path)));
  TestTranslationAvailable(browser(), "en", "ja", "downloadable");
}
}  // namespace on_device_translation
