// 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 "components/translate/content/renderer/translate_agent.h"

#include <memory>
#include <tuple>
#include <utility>

#include "base/base_paths.h"
#include "base/files/file.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback_helpers.h"
#include "base/path_service.h"
#include "base/run_loop.h"
#include "base/time/time.h"
#include "chrome/common/chrome_isolated_world_ids.h"
#include "chrome/test/base/chrome_render_view_test.h"
#include "components/language_detection/core/constants.h"
#include "components/translate/content/common/translate.mojom.h"
#include "components/translate/core/common/translate_util.h"
#include "content/public/common/url_constants.h"
#include "content/public/renderer/render_frame.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/pending_remote.h"
#include "mojo/public/cpp/bindings/receiver_set.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/browser_interface_broker_proxy.h"
#include "third_party/blink/public/platform/web_url_response.h"
#include "third_party/blink/public/web/web_document_loader.h"
#include "third_party/blink/public/web/web_local_frame.h"

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

namespace {

std::string UpdateGURLScheme(GURL url, const char scheme[]) {
  GURL::Replacements replacements;
  replacements.SetSchemeStr(scheme);
  return url.ReplaceComponents(replacements).spec();
}

class FakeContentTranslateDriver
    : public translate::mojom::ContentTranslateDriver {
 public:
  FakeContentTranslateDriver() = default;
  ~FakeContentTranslateDriver() override = default;

  void BindHandle(mojo::ScopedMessagePipeHandle handle) {
    receivers_.Add(
        this, mojo::PendingReceiver<translate::mojom::ContentTranslateDriver>(
                  std::move(handle)));
  }

  // translate::mojom::ContentTranslateDriver implementation.
  void RegisterPage(
      mojo::PendingRemote<translate::mojom::TranslateAgent> translate_agent,
      const translate::LanguageDetectionDetails& details,
      bool page_level_translation_criteria_met) override {
    called_new_page_ = true;
    details_ = details;
    page_level_translation_criteria_met_ = page_level_translation_criteria_met;
  }

  void ResetNewPageValues() {
    called_new_page_ = false;
    details_ = std::nullopt;
    page_level_translation_criteria_met_ = false;
  }

  bool called_new_page_ = false;
  bool page_level_translation_criteria_met_ = false;
  std::optional<translate::LanguageDetectionDetails> details_;

 private:
  mojo::ReceiverSet<translate::mojom::ContentTranslateDriver> receivers_;
};

// Load the model file at the provided file path.
base::File LoadModelFile(const base::FilePath& model_file_path) {
  if (!base::PathExists(model_file_path))
    return base::File();

  return base::File(model_file_path,
                    base::File::FLAG_OPEN | base::File::FLAG_READ);
}

base::FilePath model_file_path() {
  base::FilePath source_root_dir;
  base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_root_dir);
  return source_root_dir.AppendASCII("components")
      .AppendASCII("test")
      .AppendASCII("data")
      .AppendASCII("translate")
      .AppendASCII("valid_model.tflite");
}

}  // namespace

class TestTranslateAgent : public translate::TranslateAgent {
 public:
  explicit TestTranslateAgent(content::RenderFrame* render_frame)
      : translate::TranslateAgent(render_frame, ISOLATED_WORLD_ID_TRANSLATE) {}

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

  base::TimeDelta AdjustDelay(int delayInMs) override {
    // Just returns base::TimeDelta() which has initial value 0.
    // Tasks doesn't need to be delayed in tests.
    return base::TimeDelta();
  }

  void TranslatePage(const std::string& source_lang,
                     const std::string& target_lang,
                     const std::string& translate_script) {
    // Reset result values firstly.
    page_translated_ = false;
    trans_result_cancelled_ = false;
    trans_result_source_lang_ = std::nullopt;
    trans_result_translated_lang_ = std::nullopt;
    trans_result_error_type_ = translate::TranslateErrors::NONE;

    // Will get new result values via OnPageTranslated.
    TranslateFrame(translate_script, source_lang, target_lang,
                   base::BindOnce(&TestTranslateAgent::OnPageTranslated,
                                  base::Unretained(this)));
  }

  bool GetPageTranslatedResult(std::string* source_lang,
                               std::string* target_lang,
                               translate::TranslateErrors* error) {
    if (!page_translated_)
      return false;
    if (source_lang)
      *source_lang = *trans_result_source_lang_;
    if (target_lang)
      *target_lang = *trans_result_translated_lang_;
    if (error)
      *error = trans_result_error_type_;
    return true;
  }

  MOCK_METHOD0(IsTranslateLibAvailable, bool());
  MOCK_METHOD0(IsTranslateLibReady, bool());
  MOCK_METHOD0(HasTranslationFinished, bool());
  MOCK_METHOD0(HasTranslationFailed, bool());
  MOCK_METHOD0(GetPageSourceLanguage, std::string());
  MOCK_METHOD0(GetErrorCode, int64_t());
  MOCK_METHOD0(StartTranslation, bool());
  MOCK_METHOD1(ExecuteScript, void(const std::string&));
  MOCK_METHOD2(ExecuteScriptAndGetBoolResult, bool(const std::string&, bool));
  MOCK_METHOD1(ExecuteScriptAndGetStringResult,
               std::string(const std::string&));
  MOCK_METHOD1(ExecuteScriptAndGetDoubleResult, double(const std::string&));
  MOCK_METHOD1(ExecuteScriptAndGetIntegerResult, int64_t(const std::string&));

 private:
  void OnPageTranslated(bool cancelled,
                        const std::string& source_lang,
                        const std::string& translated_lang,
                        translate::TranslateErrors error_type) {
    page_translated_ = true;
    trans_result_cancelled_ = cancelled;
    trans_result_source_lang_ = source_lang;
    trans_result_translated_lang_ = translated_lang;
    trans_result_error_type_ = error_type;
  }

  bool page_translated_;
  bool trans_result_cancelled_;
  std::optional<std::string> trans_result_source_lang_;
  std::optional<std::string> trans_result_translated_lang_;
  translate::TranslateErrors trans_result_error_type_;
};

class TranslateAgentBrowserTest : public ChromeRenderViewTest {
 public:
  TranslateAgentBrowserTest() = default;

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

 protected:
  void SetUp() override {
    ChromeRenderViewTest::SetUp();
    translate_agent_ =
        std::make_unique<TestTranslateAgent>(GetMainRenderFrame());

    GetMainRenderFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
        translate::mojom::ContentTranslateDriver::Name_,
        base::BindRepeating(&FakeContentTranslateDriver::BindHandle,
                            base::Unretained(&fake_translate_driver_)));
    base::File model_file = LoadModelFile(model_file_path());
    translate_agent_->SeedLanguageDetectionModelForTesting(
        std::move(model_file));
  }

  void TearDown() override {
    GetMainRenderFrame()->GetBrowserInterfaceBroker().SetBinderForTesting(
        translate::mojom::ContentTranslateDriver::Name_, {});

    translate_agent_.reset();
    ChromeRenderViewTest::TearDown();
  }

  std::unique_ptr<TestTranslateAgent> translate_agent_;
  FakeContentTranslateDriver fake_translate_driver_;
};

// Tests that the browser gets notified of the translation failure if the
// translate library fails/times-out during initialization.
TEST_F(TranslateAgentBrowserTest, TranslateLibNeverReady) {
  // We make IsTranslateLibAvailable true so we don't attempt to inject the
  // library.
  EXPECT_CALL(*translate_agent_, IsTranslateLibAvailable())
      .Times(AtLeast(1))
      .WillRepeatedly(Return(true));

  EXPECT_CALL(*translate_agent_, IsTranslateLibReady())
      .Times(AtLeast(5))  // See kMaxTranslateInitCheckAttempts in
                          // translate_agent.cc
      .WillRepeatedly(Return(false));

  EXPECT_CALL(*translate_agent_, GetErrorCode())
      .Times(AtLeast(5))
      .WillRepeatedly(
          Return(std::to_underlying(translate::TranslateErrors::NONE)));

  translate_agent_->TranslatePage("en", "fr", std::string());
  base::RunLoop().RunUntilIdle();

  translate::TranslateErrors error;
  ASSERT_TRUE(
      translate_agent_->GetPageTranslatedResult(nullptr, nullptr, &error));
  EXPECT_EQ(translate::TranslateErrors::TRANSLATION_TIMEOUT, error);
}

// Tests that the browser gets notified of the translation success when the
// translation succeeds.
TEST_F(TranslateAgentBrowserTest, TranslateSuccess) {
  // We make IsTranslateLibAvailable true so we don't attempt to inject the
  // library.
  EXPECT_CALL(*translate_agent_, IsTranslateLibAvailable())
      .Times(AtLeast(1))
      .WillRepeatedly(Return(true));

  EXPECT_CALL(*translate_agent_, IsTranslateLibReady())
      .WillOnce(Return(false))
      .WillOnce(Return(true));

  EXPECT_CALL(*translate_agent_, GetErrorCode())
      .WillOnce(Return(std::to_underlying(translate::TranslateErrors::NONE)));

  EXPECT_CALL(*translate_agent_, StartTranslation()).WillOnce(Return(true));

  // Succeed after few checks.
  EXPECT_CALL(*translate_agent_, HasTranslationFailed())
      .WillRepeatedly(Return(false));
  EXPECT_CALL(*translate_agent_, HasTranslationFinished())
      .WillOnce(Return(false))
      .WillOnce(Return(false))
      .WillOnce(Return(true));

  // V8 call for performance monitoring should be ignored.
  EXPECT_CALL(*translate_agent_, ExecuteScriptAndGetDoubleResult(_)).Times(3);

  std::string source_lang("en");
  std::string target_lang("fr");
  translate_agent_->TranslatePage(source_lang, target_lang, std::string());
  base::RunLoop().RunUntilIdle();

  std::string received_source_lang;
  std::string received_target_lang;
  translate::TranslateErrors error;
  ASSERT_TRUE(translate_agent_->GetPageTranslatedResult(
      &received_source_lang, &received_target_lang, &error));
  EXPECT_EQ(source_lang, received_source_lang);
  EXPECT_EQ(target_lang, received_target_lang);
  EXPECT_EQ(translate::TranslateErrors::NONE, error);
}

// Tests that the browser gets notified of the translation failure when the
// translation fails.
TEST_F(TranslateAgentBrowserTest, TranslateFailure) {
  // We make IsTranslateLibAvailable true so we don't attempt to inject the
  // library.
  EXPECT_CALL(*translate_agent_, IsTranslateLibAvailable())
      .Times(AtLeast(1))
      .WillRepeatedly(Return(true));

  EXPECT_CALL(*translate_agent_, IsTranslateLibReady()).WillOnce(Return(true));

  EXPECT_CALL(*translate_agent_, StartTranslation()).WillOnce(Return(true));

  // Fail after few checks.
  EXPECT_CALL(*translate_agent_, HasTranslationFailed())
      .WillOnce(Return(false))
      .WillOnce(Return(false))
      .WillOnce(Return(false))
      .WillOnce(Return(true));

  EXPECT_CALL(*translate_agent_, HasTranslationFinished())
      .Times(AtLeast(1))
      .WillRepeatedly(Return(false));

  EXPECT_CALL(*translate_agent_, GetErrorCode())
      .WillOnce(Return(
          std::to_underlying(translate::TranslateErrors::TRANSLATION_ERROR)));

  // V8 call for performance monitoring should be ignored.
  EXPECT_CALL(*translate_agent_, ExecuteScriptAndGetDoubleResult(_)).Times(2);

  translate_agent_->TranslatePage("en", "fr", std::string());
  base::RunLoop().RunUntilIdle();

  translate::TranslateErrors error;
  ASSERT_TRUE(
      translate_agent_->GetPageTranslatedResult(nullptr, nullptr, &error));
  EXPECT_EQ(translate::TranslateErrors::TRANSLATION_ERROR, error);
}

// Tests that when the browser translate a page for which the language is
// undefined we query the translate element to get the language.
TEST_F(TranslateAgentBrowserTest, UndefinedSourceLang) {
  // We make IsTranslateLibAvailable true so we don't attempt to inject the
  // library.
  EXPECT_CALL(*translate_agent_, IsTranslateLibAvailable())
      .Times(AtLeast(1))
      .WillRepeatedly(Return(true));

  EXPECT_CALL(*translate_agent_, IsTranslateLibReady()).WillOnce(Return(true));

  EXPECT_CALL(*translate_agent_, GetPageSourceLanguage())
      .WillOnce(Return("de"));

  EXPECT_CALL(*translate_agent_, StartTranslation()).WillOnce(Return(true));
  EXPECT_CALL(*translate_agent_, HasTranslationFailed())
      .WillOnce(Return(false));
  EXPECT_CALL(*translate_agent_, HasTranslationFinished())
      .Times(AtLeast(1))
      .WillRepeatedly(Return(true));

  // V8 call for performance monitoring should be ignored.
  EXPECT_CALL(*translate_agent_, ExecuteScriptAndGetDoubleResult(_)).Times(3);

  translate_agent_->TranslatePage(language_detection::kUnknownLanguageCode,
                                  "fr", std::string());
  base::RunLoop().RunUntilIdle();

  translate::TranslateErrors error;
  std::string source_lang;
  std::string target_lang;
  ASSERT_TRUE(translate_agent_->GetPageTranslatedResult(&source_lang,
                                                        &target_lang, &error));
  EXPECT_EQ("de", source_lang);
  EXPECT_EQ("fr", target_lang);
  EXPECT_EQ(translate::TranslateErrors::NONE, error);
}

// Tests that starting a translation while a similar one is pending does not
// break anything.
TEST_F(TranslateAgentBrowserTest, MultipleSimilarTranslations) {
  // We make IsTranslateLibAvailable true so we don't attempt to inject the
  // library.
  EXPECT_CALL(*translate_agent_, IsTranslateLibAvailable())
      .Times(AtLeast(1))
      .WillRepeatedly(Return(true));

  EXPECT_CALL(*translate_agent_, IsTranslateLibReady())
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*translate_agent_, StartTranslation())
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*translate_agent_, HasTranslationFailed())
      .WillRepeatedly(Return(false));
  EXPECT_CALL(*translate_agent_, HasTranslationFinished())
      .WillOnce(Return(true));

  // V8 call for performance monitoring should be ignored.
  EXPECT_CALL(*translate_agent_, ExecuteScriptAndGetDoubleResult(_)).Times(3);

  std::string source_lang("en");
  std::string target_lang("fr");
  translate_agent_->TranslatePage(source_lang, target_lang, std::string());
  // While this is running call again TranslatePage to make sure noting bad
  // happens.
  translate_agent_->TranslatePage(source_lang, target_lang, std::string());
  base::RunLoop().RunUntilIdle();

  std::string received_source_lang;
  std::string received_target_lang;
  translate::TranslateErrors error;
  ASSERT_TRUE(translate_agent_->GetPageTranslatedResult(
      &received_source_lang, &received_target_lang, &error));
  EXPECT_EQ(source_lang, received_source_lang);
  EXPECT_EQ(target_lang, received_target_lang);
  EXPECT_EQ(translate::TranslateErrors::NONE, error);
}

// Tests that starting a translation while a different one is pending works.
TEST_F(TranslateAgentBrowserTest, MultipleDifferentTranslations) {
  EXPECT_CALL(*translate_agent_, IsTranslateLibAvailable())
      .Times(AtLeast(1))
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*translate_agent_, IsTranslateLibReady())
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*translate_agent_, StartTranslation())
      .WillRepeatedly(Return(true));
  EXPECT_CALL(*translate_agent_, HasTranslationFailed())
      .WillRepeatedly(Return(false));
  EXPECT_CALL(*translate_agent_, HasTranslationFinished())
      .WillOnce(Return(true));

  // V8 call for performance monitoring should be ignored.
  EXPECT_CALL(*translate_agent_, ExecuteScriptAndGetDoubleResult(_)).Times(5);

  std::string source_lang("en");
  std::string target_lang("fr");
  translate_agent_->TranslatePage(source_lang, target_lang, std::string());
  // While this is running call again TranslatePage with a new target lang.
  std::string new_target_lang("de");
  translate_agent_->TranslatePage(source_lang, new_target_lang, std::string());
  base::RunLoop().RunUntilIdle();

  std::string received_source_lang;
  std::string received_target_lang;
  translate::TranslateErrors error;
  ASSERT_TRUE(translate_agent_->GetPageTranslatedResult(
      &received_source_lang, &received_target_lang, &error));
  EXPECT_EQ(source_lang, received_source_lang);
  EXPECT_EQ(new_target_lang, received_target_lang);
  EXPECT_EQ(translate::TranslateErrors::NONE, error);
}

// Tests that we send the right translate language message for a page and that
// we respect the "no translate" meta-tag.
TEST_F(TranslateAgentBrowserTest, TranslatablePage) {
  LoadHTML("<html><body>A random page with random content.</body></html>");

  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_TRUE(fake_translate_driver_.page_level_translation_criteria_met_)
      << "Page should be translatable.";
  fake_translate_driver_.ResetNewPageValues();

  // Now the page specifies the META tag to prevent translation.
  LoadHTML(
      "<html><head><meta name=\"google\" value=\"notranslate\"></head>"
      "<body>A random page with random content.</body></html>");

  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_FALSE(fake_translate_driver_.page_level_translation_criteria_met_)
      << "Page should not be translatable.";
  fake_translate_driver_.ResetNewPageValues();

  // Try the alternate version of the META tag (content instead of value).
  LoadHTML(
      "<html><head><meta name=\"google\" content=\"notranslate\"></head>"
      "<body>A random page with random content.</body></html>");

  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_FALSE(fake_translate_driver_.page_level_translation_criteria_met_)
      << "Page should not be translatable.";
}

// Tests that the language meta tag takes precedence over the CLD when reporting
// the page's language.
TEST_F(TranslateAgentBrowserTest, LanguageMetaTag) {
  LoadHTML(
      "<html><head><meta http-equiv=\"content-language\" content=\"es\">"
      "</head><body>A</body></html>");
  //   "</head><body>Esta página está en español.</body></html>");

  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_EQ("es", fake_translate_driver_.details_->adopted_language);
  fake_translate_driver_.ResetNewPageValues();

  // Makes sure we support multiple languages specified.
  LoadHTML(
      "<html><head><meta http-equiv=\"content-language\" "
      "content=\" fr , es,en \">"
      "</head><body>Cette page est en français.</body></html>");

  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_EQ("fr", fake_translate_driver_.details_->adopted_language);
}

// Tests that the language meta tag works even with non-all-lower-case.
// http://code.google.com/p/chromium/issues/detail?id=145689
TEST_F(TranslateAgentBrowserTest, LanguageMetaTagCase) {
  LoadHTML(
      "<html><head><meta http-equiv=\"Content-Language\" content=\"es\">"
      "</head><body>E</body></html>");
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_EQ("es", fake_translate_driver_.details_->adopted_language);
  fake_translate_driver_.ResetNewPageValues();

  // Makes sure we support multiple languages specified.
  LoadHTML(
      "<html><head><meta http-equiv=\"Content-Language\" "
      "content=\" fr , es,en \">"
      "</head><body>Cette page est en français.</body></html>");
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_EQ("fr", fake_translate_driver_.details_->adopted_language);
}

// Tests that the language meta tag is converted to Chrome standard of dashes
// instead of underscores and proper capitalization.
// http://code.google.com/p/chromium/issues/detail?id=159487
TEST_F(TranslateAgentBrowserTest, LanguageCommonMistakesAreCorrected) {
  LoadHTML(
      "<html><head><meta http-equiv='Content-Language' content='EN_us'>"
      "</head><body>A random page with random content.</body></html>");
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_EQ("en", fake_translate_driver_.details_->adopted_language);
  fake_translate_driver_.ResetNewPageValues();

  LoadHTML(
      "<html><head><meta http-equiv='Content-Language' content='ZH_tw'>"
      "</head><body>A</body></html>");
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_EQ("zh-TW", fake_translate_driver_.details_->adopted_language);
}

// Tests that a back navigation gets a translate language message.
TEST_F(TranslateAgentBrowserTest, BackToTranslatablePage) {
  LoadHTML(
      "<html><head><meta http-equiv=\"content-language\" content=\"es\">"
      "</head><body>E</body></html>");
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_EQ("es", fake_translate_driver_.details_->adopted_language);
  fake_translate_driver_.ResetNewPageValues();

  blink::PageState back_state = GetCurrentPageState();

  LoadHTML(
      "<html><head><meta http-equiv=\"content-language\" content=\"fr\">"
      "</head><body>E</body></html>");
  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_EQ("fr", fake_translate_driver_.details_->adopted_language);
  fake_translate_driver_.ResetNewPageValues();

  GoBack(GURL("data:text/html;charset=utf-8,<html><head>"
              "<meta http-equiv=\"content-language\" content=\"es\">"
              "</head><body>E</body></html>"),
         back_state);

  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_EQ("es", fake_translate_driver_.details_->adopted_language);
}

TEST_F(TranslateAgentBrowserTest, UnsupportedTranslateSchemes) {
  GURL url("https://foo.com");
  LoadHTMLWithUrlOverride(
      "<html><body>A random page with random content.</body></html>",
      UpdateGURLScheme(url, content::kChromeUIScheme).c_str());

  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_FALSE(fake_translate_driver_.page_level_translation_criteria_met_);
  EXPECT_TRUE(fake_translate_driver_.details_->adopted_language.empty());
  fake_translate_driver_.ResetNewPageValues();

  LoadHTMLWithUrlOverride(
      "<html><body>A random page with random content.</body></html>",
      url::kAboutBlankURL);

  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_FALSE(fake_translate_driver_.page_level_translation_criteria_met_);
  EXPECT_TRUE(fake_translate_driver_.details_->adopted_language.empty());
  fake_translate_driver_.ResetNewPageValues();

  LoadHTMLWithUrlOverride(
      "<html><body>A random page with random content.</body></html>",
      UpdateGURLScheme(url, content::kChromeDevToolsScheme).c_str());

  base::RunLoop().RunUntilIdle();
  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_FALSE(fake_translate_driver_.page_level_translation_criteria_met_);
  EXPECT_TRUE(fake_translate_driver_.details_->adopted_language.empty());
}

// Tests that the agent gracefully handles being deleted while it is injecting
// the translate library script (e.g. when script execution spins a nested run
// loop that tears down the owning frame).
TEST_F(TranslateAgentBrowserTest, AgentDeletedDuringScriptInjection) {
  // Take raw ownership so the agent can be deleted from inside a mock action.
  TestTranslateAgent* agent = translate_agent_.release();

  EXPECT_CALL(*agent, IsTranslateLibAvailable()).WillOnce(Return(false));
  EXPECT_CALL(*agent, ExecuteScript(_)).WillOnce([agent](const std::string&) {
    delete agent;
  });

  agent->TranslateFrame(std::string(), "en", "fr", base::DoNothing());
}

// Tests that the agent gracefully handles being deleted while it is starting
// the translation.
TEST_F(TranslateAgentBrowserTest, AgentDeletedDuringStartTranslation) {
  TestTranslateAgent* agent = translate_agent_.release();

  EXPECT_CALL(*agent, IsTranslateLibAvailable()).WillOnce(Return(true));
  EXPECT_CALL(*agent, IsTranslateLibReady()).WillOnce(Return(true));
  EXPECT_CALL(*agent, ExecuteScriptAndGetDoubleResult(_))
      .WillRepeatedly(Return(0.0));
  EXPECT_CALL(*agent, StartTranslation()).WillOnce([agent]() {
    delete agent;
    return false;
  });

  agent->TranslateFrame(std::string(), "en", "fr", base::DoNothing());
}

// Tests that the agent gracefully handles being deleted while it is checking
// the translation status.
TEST_F(TranslateAgentBrowserTest, AgentDeletedDuringCheckTranslateStatus) {
  TestTranslateAgent* agent = translate_agent_.release();

  base::RunLoop run_loop;
  EXPECT_CALL(*agent, IsTranslateLibAvailable()).WillOnce(Return(true));
  EXPECT_CALL(*agent, IsTranslateLibReady()).WillOnce(Return(true));
  EXPECT_CALL(*agent, ExecuteScriptAndGetDoubleResult(_))
      .WillRepeatedly(Return(0.0));
  EXPECT_CALL(*agent, StartTranslation()).WillOnce(Return(true));
  EXPECT_CALL(*agent, HasTranslationFailed()).WillOnce([agent, &run_loop]() {
    delete agent;
    run_loop.Quit();
    return false;
  });

  agent->TranslateFrame(std::string(), "en", "fr", base::DoNothing());
  run_loop.Run();
}

#if BUILDFLAG(ENABLE_PDF)
TEST_F(TranslateAgentBrowserTest, PdfPageCaptured) {
  GURL url("https://example.com");
  translate_agent_->PdfPageCaptured(u"A random page with random content.", "fr",
                                    url);
  base::RunLoop().RunUntilIdle();

  ASSERT_TRUE(fake_translate_driver_.called_new_page_);
  EXPECT_TRUE(fake_translate_driver_.page_level_translation_criteria_met_);
  EXPECT_EQ("fr", fake_translate_driver_.details_->content_language);
  EXPECT_EQ("fr", fake_translate_driver_.details_->html_root_language);
  EXPECT_FALSE(fake_translate_driver_.details_->has_notranslate);
}

TEST_F(TranslateAgentBrowserTest, PdfUnsupportedTranslateSchemes) {
  GURL url("chrome://foo.com");
  translate_agent_->PdfPageCaptured(u"pdf content", "en", url);
  base::RunLoop().RunUntilIdle();

  ASSERT_FALSE(fake_translate_driver_.called_new_page_);
}

TEST_F(TranslateAgentBrowserTest, PageCapturedPdfIgnored) {
  LoadHTML("<html><body>A random page with random content.</body></html>");
  base::RunLoop().RunUntilIdle();

  fake_translate_driver_.ResetNewPageValues();

  // Manually override the MIME type to application/pdf.
  auto* main_frame = GetMainFrame();
  auto* doc_loader = main_frame->GetDocumentLoader();
  ASSERT_TRUE(doc_loader);
  const_cast<blink::WebURLResponse&>(doc_loader->GetWebResponse())
      .SetMimeType(blink::WebString::FromUtf8("application/pdf"));

  // Call PageCaptured directly.
  scoped_refptr<const base::RefCountedString16> contents =
      base::MakeRefCounted<const base::RefCountedString16>(
          u"A random page with random content.");
  translate_agent_->PageCaptured(contents);
  base::RunLoop().RunUntilIdle();

  // PageCaptured should return early and not register page.
  EXPECT_FALSE(fake_translate_driver_.called_new_page_);
}
#endif
