// Copyright 2015 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/omnibox/browser/clipboard_provider.h"

#include <memory>
#include <string>
#include <utility>

#include "base/memory/ref_counted.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "components/omnibox/browser/autocomplete_input.h"
#include "components/omnibox/browser/autocomplete_provider_listener.h"
#include "components/omnibox/browser/mock_autocomplete_provider_client.h"
#include "components/omnibox/browser/omnibox_field_trial.h"
#include "components/omnibox/browser/test_scheme_classifier.h"
#include "components/omnibox/common/omnibox_features.h"
#include "components/open_from_clipboard/fake_clipboard_recent_content.h"
#include "components/search_engines/search_engines_test_environment.h"
#include "components/search_engines/template_url_service_client.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/metrics_proto/omnibox_focus_type.pb.h"
#include "third_party/omnibox_proto/groups.pb.h"
#include "ui/gfx/image/image.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_unittest_util.h"
#include "url/gurl.h"

#if !BUILDFLAG(IS_IOS)
#include "ui/base/clipboard/test/test_clipboard.h"  // nogncheck
#endif

namespace {

const char kCurrentURL[] = "http://example.com/current";
const char kClipboardURL[] = "http://example.com/clipboard";
const char16_t kClipboardText[] = u"Search for me";

class CreateMatchWithContentCallbackWaiter {
 public:
  CreateMatchWithContentCallbackWaiter(
      scoped_refptr<ClipboardProvider> provider,
      AutocompleteMatch* match)
      : received_(false) {
    provider->UpdateClipboardMatchWithContent(
        match->GetWeakPtr(),
        base::BindOnce(&CreateMatchWithContentCallbackWaiter::OnComplete,
                       weak_ptr_factory_.GetWeakPtr()));
  }

  void WaitForMatchUpdated() {
    if (received_)
      return;

    base::RunLoop run_loop;
    quit_closure_ = run_loop.QuitClosure();
    run_loop.Run();
  }

 private:
  void OnComplete() {
    received_ = true;
    if (quit_closure_)
      std::move(quit_closure_).Run();
  }

  base::OnceClosure quit_closure_;
  bool received_;

  base::WeakPtrFactory<CreateMatchWithContentCallbackWaiter> weak_ptr_factory_{
      this};
};

}  // namespace

class ClipboardProviderTest : public testing::Test,
                              public AutocompleteProviderListener {
 public:
  ClipboardProviderTest()
      : client_(new MockAutocompleteProviderClient()),
        provider_(
            new ClipboardProvider(client_.get(), this, &clipboard_content_)) {
    SetClipboardUrl(GURL(kClipboardURL));
  }

  ~ClipboardProviderTest() override = default;

  void ClearClipboard() { clipboard_content_.SuppressClipboardContent(); }

  void SetClipboardUrl(const GURL& url) {
    clipboard_content_.SetClipboardURL(url, base::Minutes(9));
  }

  void SetClipboardText(const std::u16string& text) {
    clipboard_content_.SetClipboardText(text, base::Minutes(9));
  }

  void SetClipboardImage(const gfx::Image& image) {
    clipboard_content_.SetClipboardImage(image, base::Minutes(9));
  }

  bool IsClipboardEmpty() {
    return clipboard_content_.GetRecentURLFromClipboard() == std::nullopt &&
           clipboard_content_.GetRecentTextFromClipboard() == std::nullopt &&
           !clipboard_content_.HasRecentImageFromClipboard();
  }

  AutocompleteInput CreateAutocompleteInput(
      metrics::OmniboxFocusType focus_type) {
    AutocompleteInput input(std::u16string(), metrics::OmniboxEventProto::OTHER,
                            classifier_);
    input.set_current_url(GURL(kCurrentURL));
    input.set_focus_type(focus_type);
    return input;
  }

  void MatchesImageCallback(std::optional<AutocompleteMatch> match) {
    matches_image_match_ = match;
  }

 protected:
  // AutocompleteProviderListener:
  void OnProviderUpdate(bool updated_matches,
                        const AutocompleteProvider* provider) override;

  base::test::TaskEnvironment task_environment_;
  TestSchemeClassifier classifier_;
  FakeClipboardRecentContent clipboard_content_;
  search_engines::SearchEnginesTestEnvironment search_engines_test_environment_;
  std::unique_ptr<MockAutocompleteProviderClient> client_;
  scoped_refptr<ClipboardProvider> provider_;
  std::optional<AutocompleteMatch> matches_image_match_;
};

void ClipboardProviderTest::OnProviderUpdate(
    bool updated_matches,
    const AutocompleteProvider* provider) {
  // No action required.
}

TEST_F(ClipboardProviderTest, NotFromOmniboxFocus) {
  provider_->Start(
      CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_DEFAULT),
      false);
  EXPECT_TRUE(provider_->matches().empty());
}

TEST_F(ClipboardProviderTest, EmptyClipboard) {
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());
  ClearClipboard();
  provider_->Start(
      CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS),
      false);
  EXPECT_TRUE(provider_->matches().empty());
}

#if !BUILDFLAG(IS_ANDROID)
TEST_F(ClipboardProviderTest, MatchesImage) {
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());

  gfx::Image test_image = gfx::test::CreateImage(/*width=*/10, /*height=*/10);
  scoped_refptr<base::RefCountedMemory> image_bytes =
      provider_->EncodeClipboardImage(*test_image.ToImageSkia());
  ASSERT_TRUE(image_bytes);
  provider_->ConstructImageMatchCallback(
      base::BindOnce(&ClipboardProviderTest::MatchesImageCallback,
                     base::Unretained(this)),
      image_bytes);
  ASSERT_TRUE(matches_image_match_);
  EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_IMAGE, matches_image_match_->type);
}
#endif  // !BUILDFLAG(IS_ANDROID)

TEST_F(ClipboardProviderTest, DeleteMatch) {
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());
  SetClipboardText(kClipboardText);
  provider_->Start(
      CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS),
      false);
  ASSERT_EQ(provider_->matches().size(), 1U);

  provider_->DeleteMatch(provider_->matches().back());
  ASSERT_EQ(provider_->matches().size(), 0U);
  ASSERT_TRUE(IsClipboardEmpty());
}

TEST_F(ClipboardProviderTest, CreateBlankURLMatchOnStart) {
  SetClipboardUrl(GURL(kClipboardURL));
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());
  provider_->Start(
      CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS),
      false);
  ASSERT_GE(provider_->matches().size(), 1U);
  EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_URL,
            provider_->matches().back().type);

  // Check the match is empty.
  EXPECT_TRUE(provider_->matches().back().destination_url.is_empty());
}

TEST_F(ClipboardProviderTest, CreateBlankTextMatchOnStart) {
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());
  SetClipboardText(kClipboardText);
  provider_->Start(
      CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS),
      false);
  ASSERT_GE(provider_->matches().size(), 1U);
  EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT,
            provider_->matches().back().type);

  // Check the match is empty.
  EXPECT_TRUE(provider_->matches().back().contents.empty());
  EXPECT_TRUE(provider_->matches().back().fill_into_edit.empty());
}

TEST_F(ClipboardProviderTest, CreateBlankImageMatchOnStart) {
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());

  gfx::Image test_image = gfx::test::CreateImage(/*width=*/10, /*height=*/10);
  SetClipboardImage(test_image);
  provider_->Start(
      CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS),
      false);
  ASSERT_GE(provider_->matches().size(), 1U);
  EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_IMAGE,
            provider_->matches().back().type);
  EXPECT_FALSE(provider_->matches().back().post_content.get());
}

TEST_F(ClipboardProviderTest, SkipImageMatchGivenWantAsynchronousMatchesFalse) {
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());

  gfx::Image test_image = gfx::test::CreateImage(/*width=*/10, /*height=*/10);
  SetClipboardImage(test_image);
  // When `input.omit_asynchronous_matches` is set to true, the clipboard
  // provider should skip any asynchronous logic associated with creating an
  // image match.
  AutocompleteInput input =
      CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS);
  input.set_omit_asynchronous_matches(true);
  provider_->Start(input, false);
  ASSERT_TRUE(provider_->done());
  ASSERT_TRUE(provider_->matches().empty());
}

TEST_F(ClipboardProviderTest, CreateURLMatchWithContent) {
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());

  {
    SCOPED_TRACE(kClipboardURL);
    SetClipboardUrl(GURL(kClipboardURL));
    EXPECT_CALL(*client_.get(), GetSchemeClassifier())
        .WillOnce(testing::ReturnRef(classifier_));
    AutocompleteMatch match = provider_->NewBlankURLMatch();
    CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
    waiter.WaitForMatchUpdated();

    EXPECT_EQ(GURL(kClipboardURL), match.destination_url);
    EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_URL, match.type);
  }

  {
    SCOPED_TRACE("`javascript:` sanitization");
    SetClipboardUrl(GURL("javascript:alert()"));
    AutocompleteMatch match = provider_->NewBlankURLMatch();
    CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
    waiter.WaitForMatchUpdated();

    EXPECT_EQ(u"alert()", match.contents);
    EXPECT_EQ(u"alert()", match.fill_into_edit);
    EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
  }

  {
    SCOPED_TRACE("`JavaScript:` sanitization");
    SetClipboardUrl(GURL("JavaScript:alert()"));
    AutocompleteMatch match = provider_->NewBlankURLMatch();
    CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
    waiter.WaitForMatchUpdated();

    EXPECT_EQ(u"alert()", match.contents);
    EXPECT_EQ(u"alert()", match.fill_into_edit);
    EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
  }
  {
    SCOPED_TRACE("Match destroyed");
    SetClipboardUrl(GURL(kClipboardURL));
    EXPECT_CALL(*client_.get(), GetSchemeClassifier())
        .WillOnce(testing::ReturnRef(classifier_));
    auto match =
        std::make_unique<AutocompleteMatch>(provider_->NewBlankURLMatch());
    CreateMatchWithContentCallbackWaiter waiter(provider_, match.get());
    match.reset();
    // Should be fine.
    waiter.WaitForMatchUpdated();
  }
}

TEST_F(ClipboardProviderTest, CreateTextMatchWithContent) {
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());

  {
    SCOPED_TRACE(kClipboardText);
    SetClipboardText(kClipboardText);
    AutocompleteMatch match = provider_->NewBlankTextMatch();
    CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
    waiter.WaitForMatchUpdated();

    EXPECT_EQ(kClipboardText, match.contents);
    EXPECT_EQ(kClipboardText, match.fill_into_edit);
    EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
  }

  {
    SCOPED_TRACE("`javascript:` sanitization");
    SetClipboardText(u"javascript:alert()");
    AutocompleteMatch match = provider_->NewBlankTextMatch();
    CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
    waiter.WaitForMatchUpdated();

    EXPECT_EQ(u"alert()", match.contents);
    EXPECT_EQ(u"alert()", match.fill_into_edit);
    EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
  }

  {
    SCOPED_TRACE("`JavaScript:` sanitization");
    SetClipboardText(u"JavaScript:alert()");
    AutocompleteMatch match = provider_->NewBlankTextMatch();
    CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
    waiter.WaitForMatchUpdated();

    EXPECT_EQ(u"alert()", match.contents);
    EXPECT_EQ(u"alert()", match.fill_into_edit);
    EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
  }

  {
    SCOPED_TRACE("`javascript:javascript:` sanitization");
    SetClipboardText(u"javascript:\n javascript:alert()");
    AutocompleteMatch match = provider_->NewBlankTextMatch();
    CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
    waiter.WaitForMatchUpdated();

    EXPECT_EQ(u"alert()", match.contents);
    EXPECT_EQ(u"alert()", match.fill_into_edit);
    EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
  }
  {
    SCOPED_TRACE("Match destroyed");
    SetClipboardText(u"text");
    auto match =
        std::make_unique<AutocompleteMatch>(provider_->NewBlankTextMatch());
    CreateMatchWithContentCallbackWaiter waiter(provider_, match.get());
    match.reset();
    // Should be fine.
    waiter.WaitForMatchUpdated();
  }
}

TEST_F(ClipboardProviderTest, CreateImageMatchWithContent) {
  {
    SCOPED_TRACE("Copy Image");
    gfx::Image test_image = gfx::test::CreateImage(/*width=*/10, /*height=*/10);
    SetClipboardImage(test_image);
    client_->set_template_url_service(
        search_engines_test_environment_.template_url_service());
    AutocompleteMatch match = provider_->NewBlankImageMatch();
    CreateMatchWithContentCallbackWaiter waiter(provider_, &match);
    waiter.WaitForMatchUpdated();

    EXPECT_FALSE(match.post_content->first.empty());
    EXPECT_FALSE(match.post_content->second.empty());
    EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_IMAGE, match.type);
  }
  {
    SCOPED_TRACE("Match destroyed");
    gfx::Image test_image = gfx::test::CreateImage(/*width=*/10, /*height=*/10);
    SetClipboardImage(test_image);
    client_->set_template_url_service(
        search_engines_test_environment_.template_url_service());
    auto match =
        std::make_unique<AutocompleteMatch>(provider_->NewBlankImageMatch());
    CreateMatchWithContentCallbackWaiter waiter(provider_, match.get());
    match.reset();
    // Should be fine.
    waiter.WaitForMatchUpdated();
  }
}

#if BUILDFLAG(IS_ANDROID)
TEST_F(ClipboardProviderTest, Android_MergedWithPZPSGroupOnNTP) {
  SetClipboardText(kClipboardText);
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());

  AutocompleteInput input(std::u16string(), metrics::OmniboxEventProto::NTP,
                          classifier_);
  input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);

  provider_->Start(input, false);

  // Expect the clipboard entry, but not the content. Content is not directly
  // available on mobile devices - the user needs to explicitly ask to reveal
  // the content.
  ASSERT_EQ(provider_->matches().size(), 1U);
  const auto& match = provider_->matches().back();
  EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
  EXPECT_EQ(omnibox::GROUP_PERSONALIZED_ZERO_SUGGEST,
            match.suggestion_group_id);
}

TEST_F(ClipboardProviderTest, Android_StandaloneSuggestionOnSearchActivity) {
  SetClipboardText(kClipboardText);
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());

  AutocompleteInput input(std::u16string(),
                          metrics::OmniboxEventProto::ANDROID_SHORTCUTS_WIDGET,
                          classifier_);
  input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS);

  provider_->Start(input, false);

  // Expect the clipboard entry, but not the content. Content is not directly
  // available on mobile devices - the user needs to explicitly ask to reveal
  // the content.
  ASSERT_EQ(provider_->matches().size(), 1U);
  const auto& match = provider_->matches().back();
  EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
  EXPECT_EQ(omnibox::GROUP_MOBILE_CLIPBOARD, match.suggestion_group_id);
}

TEST_F(ClipboardProviderTest, Android_StandaloneSuggestionInNonNTPContext) {
  SetClipboardText(kClipboardText);
  client_->set_template_url_service(
      search_engines_test_environment_.template_url_service());

  AutocompleteInput input =
      CreateAutocompleteInput(metrics::OmniboxFocusType::INTERACTION_FOCUS);

  provider_->Start(input, false);

  // Expect the clipboard entry, but not the content. Content is not directly
  // available on mobile devices - the user needs to explicitly ask to reveal
  // the content.
  ASSERT_EQ(provider_->matches().size(), 1U);
  const auto& match = provider_->matches().back();
  EXPECT_EQ(AutocompleteMatchType::CLIPBOARD_TEXT, match.type);
  EXPECT_EQ(omnibox::GROUP_MOBILE_CLIPBOARD, match.suggestion_group_id);
}
#endif
