// Copyright 2023 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/actions/omnibox_action_in_suggest.h"

#include "base/memory/scoped_refptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "components/omnibox/browser/actions/omnibox_pedal_provider.h"
#include "components/omnibox/browser/mock_autocomplete_provider_client.h"
#include "components/strings/grit/components_strings.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/omnibox_proto/entity_info.pb.h"
#include "third_party/omnibox_proto/suggest_template_info.pb.h"
#include "ui/base/device_form_factor.h"
#include "ui/base/l10n/l10n_util.h"
#include "ui/base/window_open_disposition.h"
#include "url/gurl.h"

using TemplateAction = omnibox::SuggestTemplateInfo::TemplateAction;
using ActionType = omnibox::SuggestTemplateInfo_TemplateAction_ActionType;

namespace {
class FakeOmniboxAction : public OmniboxAction {
 public:
  explicit FakeOmniboxAction(OmniboxActionId id)
      : OmniboxAction(LabelStrings(u"", u"", u"", u""), GURL{}), id_(id) {}
  OmniboxActionId ActionId() const override { return id_; }

 private:
  ~FakeOmniboxAction() override = default;
  OmniboxActionId id_{};
};

// Note: can't use operator<<, because ActionType is a plain old enum.
const char* ToString(ActionType type) {
  switch (type) {
    case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CALL:
      return "Call";
    case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_DIRECTIONS:
      return "Directions";
    case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_REVIEWS:
      return "Reviews";
    case omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM:
      return "Aim";
    case omnibox::
        SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH:
      return "TabSwitch";
    default:
      NOTREACHED();
  }
}
}  // namespace

class OmniboxActionInSuggestTest : public testing::Test {
 public:
  OmniboxActionInSuggestTest();

 protected:
  MockAutocompleteProviderClient client_;
  OmniboxAction::ExecutionContext context_;
};

OmniboxActionInSuggestTest::OmniboxActionInSuggestTest()
    : context_(client_,
               OmniboxAction::ExecutionContext::OpenUrlCallback(),
               base::TimeTicks(),
               WindowOpenDisposition::IGNORE_ACTION) {}

TEST_F(OmniboxActionInSuggestTest, CheckLabelsArePresentForKnownTypes) {
  struct {
    ActionType action_type;
    int want_hint;
    int want_contents;
    int want_accessibility_focus_hint;
    int want_accessibility_activate_hint;
  } test_cases[] = {
      {
          omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CALL,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_CALL_HINT,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_CALL_CONTENTS,
          IDS_ACC_OMNIBOX_ACTION_IN_SUGGEST_SUFFIX,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_CALL_CONTENTS,
      },
      {
          omnibox::SuggestTemplateInfo_TemplateAction_ActionType_DIRECTIONS,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_DIRECTIONS_HINT,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_DIRECTIONS_CONTENTS,
          IDS_ACC_OMNIBOX_ACTION_IN_SUGGEST_SUFFIX,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_DIRECTIONS_CONTENTS,
      },
      {
          omnibox::SuggestTemplateInfo_TemplateAction_ActionType_REVIEWS,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_REVIEWS_HINT,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_REVIEWS_CONTENTS,
          IDS_ACC_OMNIBOX_ACTION_IN_SUGGEST_SUFFIX,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_REVIEWS_CONTENTS,
      },
      {
          omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_AIM_HINT,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_AIM_CONTENTS,
          IDS_ACC_OMNIBOX_ACTION_IN_SUGGEST_SUFFIX,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_AIM_CONTENTS,
      },
      {
          omnibox::
              SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_TAB_SWITCH_HINT,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_TAB_SWITCH_CONTENTS,
          IDS_ACC_OMNIBOX_ACTION_IN_SUGGEST_SUFFIX,
          IDS_OMNIBOX_ACTION_IN_SUGGEST_TAB_SWITCH_CONTENTS,
      }};

  for (const auto& test_case : test_cases) {
    TemplateAction template_action;
    template_action.set_action_type(test_case.action_type);

    auto action = base::MakeRefCounted<OmniboxActionInSuggest>(
        std::move(template_action), std::nullopt);
    EXPECT_EQ(OmniboxActionId::ACTION_IN_SUGGEST, action->ActionId())
        << "while evaluatin action " << ToString(test_case.action_type);
    EXPECT_EQ(test_case.action_type, action->Type());

    const auto& labels = action->GetLabelStrings();
    EXPECT_EQ(labels.hint, l10n_util::GetStringUTF16(test_case.want_hint))
        << "while evaluatin action " << ToString(test_case.action_type);
    EXPECT_EQ(labels.suggestion_contents,
              l10n_util::GetStringUTF16(test_case.want_contents))
        << "while evaluatin action " << ToString(test_case.action_type);
    EXPECT_EQ(
        labels.accessibility_suffix,
        l10n_util::GetStringUTF16(test_case.want_accessibility_focus_hint))
        << "while evaluatin action " << ToString(test_case.action_type);
    EXPECT_EQ(
        labels.accessibility_hint,
        l10n_util::GetStringUTF16(test_case.want_accessibility_activate_hint))
        << "while evaluatin action " << ToString(test_case.action_type);
  }
}

TEST_F(OmniboxActionInSuggestTest, ConversionFromAction) {
  const ActionType test_cases[]{
      omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CALL,
      omnibox::SuggestTemplateInfo_TemplateAction_ActionType_DIRECTIONS,
      omnibox::SuggestTemplateInfo_TemplateAction_ActionType_REVIEWS,
      omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM,
      omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH};

  for (auto test_case : test_cases) {
    TemplateAction template_action;
    template_action.set_action_type(test_case);

    scoped_refptr<OmniboxAction> upcasted_action =
        base::MakeRefCounted<OmniboxActionInSuggest>(std::move(template_action),
                                                     std::nullopt);

    auto* downcasted_action =
        OmniboxActionInSuggest::FromAction(upcasted_action.get());

    EXPECT_EQ(upcasted_action.get(), downcasted_action)
        << "while evaluatin action " << ToString(test_case);
    EXPECT_EQ(test_case, downcasted_action->Type())
        << "while evaluatin action " << ToString(test_case);
  }
}

TEST_F(OmniboxActionInSuggestTest, ConversionFromNonAction) {
  const OmniboxActionId test_cases[]{OmniboxActionId::HISTORY_CLUSTERS,
                                     OmniboxActionId::PEDAL,
                                     OmniboxActionId::TAB_SWITCH};

  for (auto test_case : test_cases) {
    auto fake_action = base::MakeRefCounted<FakeOmniboxAction>(test_case);
    EXPECT_EQ(nullptr, OmniboxActionInSuggest::FromAction(fake_action.get()));
  }
}

TEST_F(OmniboxActionInSuggestTest, AllDeclaredActionTypesAreProperlyReflected) {
  // This test verifies that we're not quietly migrating new action types, and
  // failing to recognize the need for appropriate coverage, both in terms of
  // labels (hints, accessibility) but also UMA metrics.
  for (int type = TemplateAction::ActionType_MIN;
       type <= TemplateAction::ActionType_MAX; type++) {
    if (omnibox::SuggestTemplateInfo_TemplateAction_ActionType_IsValid(type)) {
      TemplateAction template_action;
      template_action.set_action_type(ActionType(type));

      // This is a valid action type. Object MUST build.
      auto action = base::MakeRefCounted<OmniboxActionInSuggest>(
          std::move(template_action), std::nullopt);
      // This is a valid action type. Object MUST be able to report metrics.
      {
        base::HistogramTester histograms;
        action->RecordActionShown(1, false);
        // NO actions report to the 'Unknown' bucket.
        histograms.ExpectBucketCount("Omnibox.ActionInSuggest.Shown", 0, 0);
        histograms.ExpectBucketCount("Omnibox.ActionInSuggest.Used", 0, 0);
        histograms.ExpectTotalCount("Omnibox.ActionInSuggest.Shown", 1);
        histograms.ExpectTotalCount("Omnibox.ActionInSuggest.Used", 0);
      }

      {
        base::HistogramTester histograms;
        action->RecordActionShown(1, true);
        // NO actions report to the 'Unknown' bucket.
        histograms.ExpectBucketCount("Omnibox.ActionInSuggest.Shown", 0, 0);
        histograms.ExpectBucketCount("Omnibox.ActionInSuggest.Used", 0, 0);
        histograms.ExpectTotalCount("Omnibox.ActionInSuggest.Shown", 1);
        histograms.ExpectTotalCount("Omnibox.ActionInSuggest.Used", 1);
      }
    }
  }
}

TEST_F(OmniboxActionInSuggestTest, HistogramsRecording) {
  enum class UmaTypeForTest {
    kUnknown = 0,
    kCall,
    kDirections,
    kWebsite,
    kReviews,
    kAim,
    kLens,
    kTabSwitch,
  };

  // Correlation between ActionType and UMA-recorded bucket.
  struct {
    omnibox::SuggestTemplateInfo::TemplateAction::ActionType type;
    UmaTypeForTest recordedUmaType;
    const char* dedicatedHistogram;
  } test_cases[]{
      {omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CALL,
       UmaTypeForTest::kCall, "Omnibox.ActionInSuggest.UsageByType.Call"},
      {omnibox::SuggestTemplateInfo_TemplateAction_ActionType_DIRECTIONS,
       UmaTypeForTest::kDirections,
       "Omnibox.ActionInSuggest.UsageByType.Directions"},
      {omnibox::SuggestTemplateInfo_TemplateAction_ActionType_REVIEWS,
       UmaTypeForTest::kReviews, "Omnibox.ActionInSuggest.UsageByType.Reviews"},
      {omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM,
       UmaTypeForTest::kAim, "Omnibox.ActionInSuggest.UsageByType.AIM"},
      {omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH,
       UmaTypeForTest::kTabSwitch,
       "Omnibox.ActionInSuggest.UsageByType.TabSwitch"},
  };

  for (const auto& test_case : test_cases) {
    TemplateAction template_action;
    template_action.set_action_type(test_case.type);
    scoped_refptr<OmniboxAction> action =
        base::MakeRefCounted<OmniboxActionInSuggest>(std::move(template_action),
                                                     std::nullopt);

    {
      // Just show.
      base::HistogramTester histograms;
      action->RecordActionShown(1, false);
      histograms.ExpectBucketCount("Omnibox.ActionInSuggest.Shown",
                                   test_case.recordedUmaType, 1);
      histograms.ExpectBucketCount("Omnibox.ActionInSuggest.Used",
                                   test_case.recordedUmaType, 0);
      histograms.ExpectTotalCount("Omnibox.ActionInSuggest.Shown", 1);
      histograms.ExpectTotalCount("Omnibox.ActionInSuggest.Used", 0);

      histograms.ExpectBucketCount(test_case.dedicatedHistogram, false, 1);
      histograms.ExpectTotalCount(test_case.dedicatedHistogram, 1);
    }

    {
      // Show and execute.
      base::HistogramTester histograms;
      action->RecordActionShown(1, true);
      histograms.ExpectBucketCount("Omnibox.ActionInSuggest.Shown",
                                   test_case.recordedUmaType, 1);
      histograms.ExpectBucketCount("Omnibox.ActionInSuggest.Used",
                                   test_case.recordedUmaType, 1);
      histograms.ExpectTotalCount("Omnibox.ActionInSuggest.Shown", 1);
      histograms.ExpectTotalCount("Omnibox.ActionInSuggest.Used", 1);

      histograms.ExpectBucketCount(test_case.dedicatedHistogram, true, 1);
      histograms.ExpectTotalCount(test_case.dedicatedHistogram, 1);
    }
  }
}

TEST_F(OmniboxActionInSuggestTest, ShowAsActionButton) {
  for (int type = TemplateAction::ActionType_MIN;
       type <= TemplateAction::ActionType_MAX; type++) {
    if (omnibox::SuggestTemplateInfo_TemplateAction_ActionType_IsValid(type) &&
        type !=
            omnibox::
                SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH) {
      TemplateAction template_action;
      template_action.set_action_type(ActionType(type));
      auto action = base::MakeRefCounted<OmniboxActionInSuggest>(
          std::move(template_action), std::nullopt);
      if (type ==
          omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_AIM) {
        EXPECT_EQ(ActionPresentationMode::BUTTON, action->presentation_mode_);
      } else {
        EXPECT_EQ(ActionPresentationMode::CHIP, action->presentation_mode_);
      }
    }
  }
}

#if BUILDFLAG(IS_ANDROID)
TEST_F(OmniboxActionInSuggestTest, ShowAsActionButtonForTabSwitch) {
  TemplateAction template_action;
  template_action.set_action_type(
      omnibox::SuggestTemplateInfo_TemplateAction_ActionType_CHROME_TAB_SWITCH);
  auto action = base::MakeRefCounted<OmniboxActionInSuggest>(
      std::move(template_action), std::nullopt);
  auto form_factor = ui::GetDeviceFormFactor();
  EXPECT_EQ(form_factor == ui::DEVICE_FORM_FACTOR_PHONE ||
                    form_factor == ui::DEVICE_FORM_FACTOR_FOLDABLE
                ? ActionPresentationMode::BUTTON
                : ActionPresentationMode::CHIP,
            action->presentation_mode_);
}
#endif
