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

#include "chrome/browser/spellchecker/spellcheck_service.h"

#include <algorithm>
#include <optional>
#include <ostream>

#include "base/command_line.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/process/kill.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/supports_user_data.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "build/build_config.h"
#include "chrome/browser/spellchecker/spellcheck_factory.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_profile.h"
#include "components/language/core/browser/pref_names.h"
#include "components/prefs/testing_pref_service.h"
#include "components/spellcheck/browser/pref_names.h"
#include "components/spellcheck/browser/spellcheck_platform.h"
#include "components/spellcheck/common/spellcheck.mojom.h"
#include "components/spellcheck/common/spellcheck_features.h"
#include "components/user_prefs/user_prefs.h"
#include "content/public/test/browser_task_environment.h"
#include "content/public/test/mock_render_process_host.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "testing/gtest/include/gtest/gtest.h"

struct TestCase {
  TestCase(
      const std::string& accept_languages,
      const std::vector<std::string>& spellcheck_dictionaries,
      const std::vector<std::string>& expected_languages,
      const std::vector<std::string>& expected_languages_used_for_spellcheck)
      : accept_languages(accept_languages),
        spellcheck_dictionaries(spellcheck_dictionaries) {
    SpellcheckService::Dictionary dictionary;
    for (const auto& language : expected_languages) {
      if (!language.empty()) {
        dictionary.language = language;
        dictionary.used_for_spellcheck = std::ranges::contains(
            expected_languages_used_for_spellcheck, language);
        expected_dictionaries.push_back(dictionary);
      }
    }
  }

  ~TestCase() = default;

  std::string accept_languages;
  std::vector<std::string> spellcheck_dictionaries;
  std::vector<SpellcheckService::Dictionary> expected_dictionaries;
};

bool operator==(const SpellcheckService::Dictionary& lhs,
                const SpellcheckService::Dictionary& rhs) {
  return lhs.language == rhs.language &&
         lhs.used_for_spellcheck == rhs.used_for_spellcheck;
}

std::ostream& operator<<(std::ostream& out,
                         const SpellcheckService::Dictionary& dictionary) {
  out << "{\"" << dictionary.language << "\", used_for_spellcheck="
      << (dictionary.used_for_spellcheck ? "true " : "false") << "}";
  return out;
}

std::ostream& operator<<(std::ostream& out, const TestCase& test_case) {
  out << "language::prefs::kAcceptLanguages=[" << test_case.accept_languages
      << "], prefs::kSpellCheckDictionaries=["
      << base::JoinString(test_case.spellcheck_dictionaries, ",")
      << "], expected=[";
  for (const auto& dictionary : test_case.expected_dictionaries) {
    out << dictionary << ",";
  }
  out << "]";
  return out;
}

static std::unique_ptr<KeyedService> BuildSpellcheckService(
    content::BrowserContext* profile) {
  return std::make_unique<SpellcheckService>(static_cast<Profile*>(profile));
}

class SpellcheckServiceUnitTestBase : public testing::Test {
 public:
  SpellcheckServiceUnitTestBase() = default;

  SpellcheckServiceUnitTestBase(const SpellcheckServiceUnitTestBase&) = delete;
  SpellcheckServiceUnitTestBase& operator=(
      const SpellcheckServiceUnitTestBase&) = delete;

  ~SpellcheckServiceUnitTestBase() override = default;

  content::BrowserContext* browser_context() { return &profile_; }
  PrefService* prefs() { return profile_.GetPrefs(); }

 protected:
  void SetUp() override {
    // Use SetTestingFactoryAndUse to force creation and initialization.
    SpellcheckServiceFactory::GetInstance()->SetTestingFactoryAndUse(
        &profile_, base::BindRepeating(&BuildSpellcheckService));
  }

  content::BrowserTaskEnvironment task_environment_;

  TestingProfile profile_;
};

class SpellcheckServiceUnitTest : public SpellcheckServiceUnitTestBase,
                                  public testing::WithParamInterface<TestCase> {
 private:
#if BUILDFLAG(IS_WIN)
  // Tests were designed assuming Hunspell dictionary used and may fail when
  // Windows spellcheck is enabled by default.
  spellcheck::ScopedDisableBrowserSpellCheckerForTesting
      disable_browser_spell_checker_;
#endif  // BUILDFLAG(IS_WIN)
};

INSTANTIATE_TEST_SUITE_P(
    TestCases,
    SpellcheckServiceUnitTest,
    testing::Values(
        TestCase("en-JP,aa", {"aa"}, {}, {}),
        TestCase("en,aa", {"aa"}, {"en"}, {}),
        TestCase("en,en-JP,fr,aa", {"fr"}, {"en", "fr"}, {"fr"}),
        TestCase("en,en-JP,fr,zz,en-US", {"fr"}, {"en", "fr", "en-US"}, {"fr"}),
        TestCase("en,en-US,en-GB",
                 {"en-GB"},
                 {"en", "en-US", "en-GB"},
                 {"en-GB"}),
        TestCase("en,en-US,en-AU",
                 {"en-AU"},
                 {"en", "en-US", "en-AU"},
                 {"en-AU"}),
        TestCase("en,en-US,en-AU",
                 {"en-US"},
                 {"en", "en-US", "en-AU"},
                 {"en-US"}),
        TestCase("en,en-US", {"en-US"}, {"en", "en-US"}, {"en-US"}),
        TestCase("en,en-US,fr", {"en-US"}, {"en", "en-US", "fr"}, {"en-US"}),
        TestCase("en,fr,en-US,en-AU",
                 {"en-US", "fr"},
                 {"en", "fr", "en-US", "en-AU"},
                 {"fr", "en-US"}),
        TestCase("en-US,en", {"en-US"}, {"en-US", "en"}, {"en-US"}),
#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
        // Scenario where user disabled the Windows spellcheck feature with some
        // non-Hunspell languages set in preferences.
        TestCase("fr,eu,en-US,ar",
                 {"fr", "eu", "en-US", "ar"},
                 {"fr", "en-US"},
                 {"fr", "en-US"}),
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
        TestCase("hu-HU,hr-HR", {"hr"}, {"hu", "hr"}, {"hr"})));

TEST_P(SpellcheckServiceUnitTest, GetDictionaries) {
  prefs()->SetString(language::prefs::kAcceptLanguages,
                     GetParam().accept_languages);
  base::ListValue spellcheck_dictionaries;
  for (const std::string& dictionary : GetParam().spellcheck_dictionaries) {
    spellcheck_dictionaries.Append(dictionary);
  }
  prefs()->SetList(spellcheck::prefs::kSpellCheckDictionaries,
                   std::move(spellcheck_dictionaries));

  std::vector<SpellcheckService::Dictionary> dictionaries;
  SpellcheckService::GetDictionaries(browser_context(), &dictionaries);

  EXPECT_EQ(GetParam().expected_dictionaries, dictionaries);
}

// Observes the SpellChecker interface for a single renderer. A unit test has no
// real renderers, so the MockRenderProcessHost below is the only host
// InitForAllRenderers() can find, which makes an Initialize() call
// unambiguously attributable to it.
class SpellcheckServiceRendererInitUnitTest
    : public SpellcheckServiceUnitTestBase,
      public spellcheck::mojom::SpellChecker {
 public:
  SpellcheckServiceRendererInitUnitTest() = default;

  SpellcheckServiceRendererInitUnitTest(
      const SpellcheckServiceRendererInitUnitTest&) = delete;
  SpellcheckServiceRendererInitUnitTest& operator=(
      const SpellcheckServiceRendererInitUnitTest&) = delete;

 protected:
  void SetUp() override {
    SpellcheckServiceUnitTestBase::SetUp();
    SpellcheckService::OverrideBinderForTesting(base::BindRepeating(
        &SpellcheckServiceRendererInitUnitTest::Bind, base::Unretained(this)));
    renderer_ = std::make_unique<content::MockRenderProcessHost>(&profile_);
    renderer_->Init();
  }

  void TearDown() override {
    receivers_.Clear();
    renderer_.reset();
    SpellcheckService::OverrideBinderForTesting(base::NullCallback());
  }

  content::MockRenderProcessHost* renderer() { return renderer_.get(); }

  // Waits until at least `count` Initialize() calls have arrived. Returns
  // false on timeout.
  [[nodiscard]] bool WaitForInitializeCount(int count) {
    return base::test::RunUntil(
        [this, count]() { return initialize_count_ >= count; });
  }

 private:
  void Bind(mojo::PendingReceiver<spellcheck::mojom::SpellChecker> receiver) {
    // A ReceiverSet, not a single Receiver: InitForAllRenderers() may reach
    // several hosts, and rebinding a single Receiver would close the earlier
    // pipe before its Initialize() call was delivered, silently losing it.
    receivers_.Add(this, std::move(receiver));
  }

  // spellcheck::mojom::SpellChecker:
  void Initialize(
      std::vector<spellcheck::mojom::SpellCheckBDictLanguagePtr> dictionaries,
      const std::vector<std::string>& custom_words,
      bool enable) override {
    ++initialize_count_;
  }
  void CustomDictionaryChanged(
      const std::vector<std::string>& words_added,
      const std::vector<std::string>& words_removed) override {}

  std::unique_ptr<content::MockRenderProcessHost> renderer_;
  mojo::ReceiverSet<spellcheck::mojom::SpellChecker> receivers_;
  int initialize_count_ = 0;

#if BUILDFLAG(IS_WIN)
  // This test assumes the Hunspell code path.
  spellcheck::ScopedDisableBrowserSpellCheckerForTesting
      disable_browser_spell_checker_;
#endif  // BUILDFLAG(IS_WIN)
};

// A renderer that has been initialized but whose process is still launching has
// no process handle yet. It must still be initialized, because
// InitForAllRenderers() is the only path that delivers spellcheck state to a
// renderer that was contacted before its dictionaries were ready.
TEST_F(SpellcheckServiceRendererInitUnitTest, ReachesStillLaunchingRenderer) {
  // Reproduce the window between RenderProcessHostImpl::Init() and
  // OnProcessLaunched().
  renderer()->SimulateProcessStillLaunchingForTesting(true);
  ASSERT_FALSE(renderer()->GetProcess().Handle());
  ASSERT_TRUE(renderer()->IsInitializedAndNotDead());

  // Flipping the pref runs InitForAllRenderers().
  prefs()->SetBoolean(spellcheck::prefs::kSpellCheckEnable, false);

  EXPECT_TRUE(WaitForInitializeCount(1))
      << "a still-launching renderer was never initialized";
}

#if BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
class SpellcheckServiceHybridUnitTestBase
    : public SpellcheckServiceUnitTestBase {
 public:
  SpellcheckServiceHybridUnitTestBase() = default;

 protected:
  void SetUp() override {
    // Use SetTestingFactoryAndUse to force creation and initialization.
    SpellcheckServiceFactory::GetInstance()->SetTestingFactoryAndUse(
        &profile_, base::BindRepeating(&BuildSpellcheckService));
  }

  virtual void InitializeSpellcheckService(
      const std::vector<std::string>& spellcheck_languages_for_testing) {
    // Fake the presence of Windows spellcheck dictionaries.
    spellcheck_service_ =
        SpellcheckServiceFactory::GetInstance()->GetForContext(
            browser_context());

    spellcheck_service_->InitWindowsDictionaryLanguages(
        spellcheck_languages_for_testing);

    ASSERT_TRUE(spellcheck_service_->dictionaries_loaded());
  }

  void RunGetDictionariesTest(
      const std::string accept_languages,
      const std::vector<std::string> spellcheck_dictionaries,
      const std::vector<SpellcheckService::Dictionary> expected_dictionaries);

  void RunDictionaryMappingTest(
      const std::string full_tag,
      const std::string expected_accept_language,
      const std::string expected_tag_passed_to_spellcheck,
      const std::string expected_accept_language_generic,
      const std::string expected_tag_passed_to_spellcheck_generic);

  // Used for faking the presence of Windows spellcheck dictionaries.
  static const std::vector<std::string>
      windows_spellcheck_languages_for_testing_;

  raw_ptr<SpellcheckService> spellcheck_service_;
};

void SpellcheckServiceHybridUnitTestBase::RunGetDictionariesTest(
    const std::string accept_languages,
    const std::vector<std::string> spellcheck_dictionaries,
    const std::vector<SpellcheckService::Dictionary> expected_dictionaries) {
  prefs()->SetString(language::prefs::kAcceptLanguages, accept_languages);
  base::ListValue spellcheck_dictionaries_list;
  for (std::string dict : spellcheck_dictionaries) {
    spellcheck_dictionaries_list.Append(dict);
  }
  prefs()->SetList(spellcheck::prefs::kSpellCheckDictionaries,
                   std::move(spellcheck_dictionaries_list));

  // Simulate first-run scenario (method is normally called during browser
  // start-up). If the primary accept language has no dictionary support, it is
  // expected that spellchecking will be disabled for that language.
  SpellcheckService::EnableFirstUserLanguageForSpellcheck(prefs());

  InitializeSpellcheckService(windows_spellcheck_languages_for_testing_);

  std::vector<SpellcheckService::Dictionary> dictionaries;
  SpellcheckService::GetDictionaries(browser_context(), &dictionaries);

  EXPECT_EQ(expected_dictionaries, dictionaries);
}

void SpellcheckServiceHybridUnitTestBase::RunDictionaryMappingTest(
    const std::string full_tag,
    const std::string expected_accept_language,
    const std::string expected_tag_passed_to_spellcheck,
    const std::string expected_accept_language_generic,
    const std::string expected_tag_passed_to_spellcheck_generic) {
  InitializeSpellcheckService({full_tag});

  std::string supported_dictionary;
  if (!expected_accept_language.empty()) {
    supported_dictionary =
        spellcheck_service_->GetSupportedWindowsDictionaryLanguage(
            expected_accept_language);
    EXPECT_FALSE(supported_dictionary.empty());
    EXPECT_EQ(full_tag, supported_dictionary);
    EXPECT_EQ(expected_tag_passed_to_spellcheck,
              SpellcheckService::GetTagToPassToWindowsSpellchecker(
                  expected_accept_language, full_tag));

    // Special case for Serbian. The "sr" accept language is interpreted as
    // using Cyrillic script. There should be an extra entry in the windows
    // dictionary map if Cyrillic windows dictionary is installed.
    if (base::EqualsCaseInsensitiveASCII(
            "sr-Cyrl", SpellcheckService::GetLanguageAndScriptTag(
                           full_tag,
                           /* include_script_tag= */ true))) {
      EXPECT_EQ(
          full_tag,
          spellcheck_service_->GetSupportedWindowsDictionaryLanguage("sr"));
    } else {
      EXPECT_TRUE(
          spellcheck_service_->GetSupportedWindowsDictionaryLanguage("sr")
              .empty());
    }

    if (!expected_accept_language_generic.empty()) {
      supported_dictionary =
          spellcheck_service_->GetSupportedWindowsDictionaryLanguage(
              expected_accept_language_generic);
      EXPECT_FALSE(supported_dictionary.empty());
      EXPECT_EQ(expected_accept_language_generic, supported_dictionary);
      EXPECT_EQ(expected_tag_passed_to_spellcheck_generic,
                SpellcheckService::GetTagToPassToWindowsSpellchecker(
                    expected_accept_language_generic, supported_dictionary));
    } else {
      // Should only be one entry in the map.
      EXPECT_EQ(1u,
                spellcheck_service_->windows_spellcheck_dictionary_map_.size());
    }
  } else {
    // Unsupported language--should not be in map.
    EXPECT_TRUE(
        spellcheck_service_->windows_spellcheck_dictionary_map_.empty());
  }
}

// static
const std::vector<std::string> SpellcheckServiceHybridUnitTestBase::
    windows_spellcheck_languages_for_testing_ = {
        "fr-FR",   // Has both Windows and Hunspell support.
        "es-MX",   // Has both Windows and Hunspell support, but for Hunspell
                   // maps to es-ES.
        "gl-ES",   // (Galician) Has only Windows support, no Hunspell
                   // dictionary.
        "fi-FI",   // (Finnish) Has only Windows support, no Hunspell
                   // dictionary.
        "it-IT",   // Has both Windows and Hunspell support.
        "pt-BR",   // Has both Windows and Hunspell support.
        "haw-US",  // (Hawaiian) No Hunspell dictionary. Note that first two
                   // letters of language code are "ha," the same as Hausa.
        "ast",     // (Asturian) Has only Windows support, no Hunspell
                   // dictionary. Note that last two letters of language
                   // code are "st," the same as Sesotho.
        "kok-Deva-IN",       // Konkani (Devanagari, India)--note presence of
                             // script subtag.
        "sr-Cyrl-ME",        // Serbian (Cyrillic, Montenegro)--note presence of
                             // script subtag.
        "sr-Latn-ME",        // Serbian (Latin, Montenegro)--note presence of
                             // script subtag.
        "ja-Latn-JP-x-ext",  // Japanese with Latin script--note presence of
                             // private use subtag. Ignore private use
                             // dictionaries.
};

static const TestCase kHybridGetDictionariesParams[] = {
    // Galician (gl) has only Windows support, no Hunspell dictionary. Croatian
    // (hr) has only Hunspell support, no local Windows dictionary. First
    // language is supported by windows and should be spellchecked.
    TestCase("gl", {}, {"gl"}, {"gl"}),
    TestCase("gl", {"gl"}, {"gl"}, {"gl"}),
    TestCase("gl,hr", {}, {"gl", "hr"}, {"gl"}),
    TestCase("gl,hr", {"gl"}, {"gl", "hr"}, {"gl"}),
    TestCase("gl,hr", {"hr"}, {"gl", "hr"}, {"gl", "hr"}),
    TestCase("gl,hr", {"gl", "hr"}, {"gl", "hr"}, {"gl", "hr"}),
    TestCase("hr", {}, {"hr"}, {"hr"}),
    TestCase("hr", {"hr"}, {"hr"}, {"hr"}),
    TestCase("hr,gl", {"hr"}, {"hr", "gl"}, {"hr"}),
    // Cebuano (ceb) is a language with neither Windows or Hunspell support,
    // should be unset if was enabled during simulated "first run" scenario.
    TestCase("ceb", {}, {}, {}),
    TestCase("ceb,gl,hr", {"gl", "hr"}, {"gl", "hr"}, {"gl", "hr"}),
    // Finnish has only "fi" in hard-coded list of accept languages.
    TestCase("fi-FI,fi,en-US,en",
             {"en-US"},
             {"fi", "en-US", "en"},
             {"fi", "en-US"}),
    // First language is supported by Windows but private use dictionaries
    // are ignored.
    TestCase("ja,gl", {"gl"}, {"gl"}, {"gl"}),
    // (Basque) No Hunspell support, has Windows support but
    // language pack not present.
    TestCase("eu", {"eu"}, {}, {}),
    TestCase("es-419,es-MX",
             {"es-419", "es-MX"},
             {"es-419", "es-MX"},
             {"es-419", "es-MX"}),
    TestCase("fr-FR,es-MX,gl,pt-BR,hr,it",
             {"fr-FR", "gl", "pt-BR", "it"},
             {"fr-FR", "es-MX", "gl", "pt-BR", "hr", "it"},
             {"fr-FR", "gl", "pt-BR", "it"}),
    // Hausa with Hawaiian language pack (ha/haw string in string).
    TestCase("ha", {"ha"}, {}, {}),
    // Sesotho with Asturian language pack (st/ast string in string).
    TestCase("st", {"st"}, {}, {}),
    // User chose generic Serbian in languages preferences (which uses
    // Cyrillic script).
    TestCase("sr,sr-Latn-RS", {"sr", "sr-Latn-RS"}, {"sr"}, {"sr"}),
    // If there is platform spellcheck support for a regional variation of
    // a language, the generic version should also be toggleable in spellcheck
    // settings. There is no Hunspell dictionary for generic Portuguese (pt);
    // there is Hunspell support for generic Italian (it) but the platform
    // dictionary should be used instead.
    TestCase("pt,pt-BR", {"pt", "pt-BR"}, {"pt", "pt-BR"}, {"pt", "pt-BR"}),
    TestCase("it,it-IT", {"it", "it-IT"}, {"it", "it-IT"}, {"it", "it-IT"}),
};

struct DictionaryMappingTestCase {
  std::string full_tag;
  std::string expected_accept_language;
  std::string expected_tag_passed_to_spellcheck;
  std::string expected_accept_language_generic;
  std::string expected_tag_passed_to_spellcheck_generic;
};

std::ostream& operator<<(std::ostream& out,
                         const DictionaryMappingTestCase& test_case) {
  out << "full_tag=" << test_case.full_tag
      << ", expected_accept_language=" << test_case.expected_accept_language
      << ", expected_tag_passed_to_spellcheck="
      << test_case.expected_tag_passed_to_spellcheck
      << ", expected_accept_language_generic="
      << test_case.expected_accept_language_generic
      << ", expected_tag_passed_to_spellcheck_generic="
      << test_case.expected_tag_passed_to_spellcheck_generic;

  return out;
}

static const DictionaryMappingTestCase kHybridDictionaryMappingsParams[] = {
    DictionaryMappingTestCase({"en-CA", "en-CA", "en-CA", "en", "en"}),
    DictionaryMappingTestCase({"en-PH", "en", "en", "", ""}),
    DictionaryMappingTestCase({"es-MX", "es-MX", "es-MX", "es", "es"}),
    DictionaryMappingTestCase({"ar-SA", "ar", "ar", "", ""}),
    DictionaryMappingTestCase({"kok-Deva-IN", "kok", "kok-Deva", "", ""}),
    DictionaryMappingTestCase({"sr-Cyrl-RS", "sr", "sr-Cyrl", "", ""}),
    DictionaryMappingTestCase({"sr-Cyrl-ME", "sr", "sr-Cyrl", "", ""}),
    // Only sr with Cyrillic implied supported in Chromium.
    DictionaryMappingTestCase({"sr-Latn-RS", "", "sr-Latn", "", ""}),
    DictionaryMappingTestCase({"sr-Latn-ME", "", "sr-Latn", "", ""}),
    DictionaryMappingTestCase({"ca-ES", "ca", "ca", "", ""}),
    DictionaryMappingTestCase({"ca-ES-valencia", "ca", "ca", "", ""}),
    // If there is platform spellcheck support for a regional variation of
    // a language, the generic version should also be toggleable in spellcheck
    // settings. There is no Hunspell dictionary for generic Portuguese (pt);
    // there is Hunspell support for generic Italian (it) but the platform
    // dictionary should be used instead.
    DictionaryMappingTestCase({"it-IT", "it-IT", "it-IT", "it", "it"}),
    DictionaryMappingTestCase({"pt-BR", "pt-BR", "pt-BR", "pt", "pt"}),
};

class SpellcheckServiceHybridUnitTestDelayInitBase
    : public SpellcheckServiceHybridUnitTestBase {
 public:
  SpellcheckServiceHybridUnitTestDelayInitBase() = default;

  void OnDictionariesInitialized() {
    dictionaries_initialized_received_ = true;
    if (quit_)
      std::move(quit_).Run();
  }

 protected:
  void InitializeSpellcheckService(
      const std::vector<std::string>& spellcheck_languages_for_testing)
      override {
    // Fake the presence of Windows spellcheck dictionaries.
    spellcheck_service_ =
        SpellcheckServiceFactory::GetInstance()->GetForContext(
            browser_context());

    spellcheck_service_->AddSpellcheckLanguagesForTesting(
        spellcheck_languages_for_testing);

    // Asynchronously load the dictionaries.
    ASSERT_FALSE(spellcheck_service_->dictionaries_loaded());
    spellcheck_service_->InitializeDictionaries(
        base::BindOnce(&SpellcheckServiceHybridUnitTestDelayInitBase::
                           OnDictionariesInitialized,
                       base::Unretained(this)));

    RunUntilCallbackReceived();
    ASSERT_TRUE(spellcheck_service_->dictionaries_loaded());
  }

  void RunUntilCallbackReceived() {
    if (dictionaries_initialized_received_)
      return;
    base::RunLoop run_loop;
    quit_ = run_loop.QuitClosure();
    run_loop.Run();

    // reset status.
    dictionaries_initialized_received_ = false;
  }

 private:
  bool dictionaries_initialized_received_ = false;

  // Quits the RunLoop on receiving the callback from InitializeDictionaries.
  base::OnceClosure quit_;
};

class SpellcheckServiceHybridUnitTestDelayInit
    : public SpellcheckServiceHybridUnitTestDelayInitBase,
      public testing::WithParamInterface<TestCase> {};

INSTANTIATE_TEST_SUITE_P(TestCases,
                         SpellcheckServiceHybridUnitTestDelayInit,
                         testing::ValuesIn(kHybridGetDictionariesParams));

TEST_P(SpellcheckServiceHybridUnitTestDelayInit, GetDictionaries) {
  RunGetDictionariesTest(GetParam().accept_languages,
                         GetParam().spellcheck_dictionaries,
                         GetParam().expected_dictionaries);
}

class SpellcheckServiceWindowsDictionaryMappingUnitTestDelayInit
    : public SpellcheckServiceHybridUnitTestDelayInitBase,
      public testing::WithParamInterface<DictionaryMappingTestCase> {};

INSTANTIATE_TEST_SUITE_P(
    TestCases,
    SpellcheckServiceWindowsDictionaryMappingUnitTestDelayInit,
    testing::ValuesIn(kHybridDictionaryMappingsParams));

TEST_P(SpellcheckServiceWindowsDictionaryMappingUnitTestDelayInit,
       CheckMappings) {
  RunDictionaryMappingTest(
      GetParam().full_tag, GetParam().expected_accept_language,
      GetParam().expected_tag_passed_to_spellcheck,
      GetParam().expected_accept_language_generic,
      GetParam().expected_tag_passed_to_spellcheck_generic);
}
#endif  // BUILDFLAG(IS_WIN) && BUILDFLAG(USE_BROWSER_SPELLCHECKER)
