// 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 "chrome/browser/ui/autofill/payments/mandatory_reauth_bubble_controller_impl.h"

#include "base/functional/bind.h"
#include "base/strings/strcat.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/mock_callback.h"
#include "chrome/browser/ui/autofill/autofill_bubble_base.h"
#include "chrome/browser/ui/autofill/bubble_manager.h"
#include "chrome/browser/ui/autofill/mock_bubble_manager.h"
#include "chrome/browser/ui/autofill/test/test_autofill_bubble_handler.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/test/base/chrome_render_view_host_test_harness.h"
#include "components/autofill/core/browser/metrics/payments/mandatory_reauth_metrics.h"
#include "components/autofill/core/browser/test_utils/autofill_test_utils.h"
#include "components/tabs/public/mock_tab_interface.h"
#include "components/tabs/public/tab_interface.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace autofill {

class TestMandatoryReauthBubbleControllerImpl
    : public MandatoryReauthBubbleControllerImpl {
 public:
  static void CreateForTesting(content::WebContents* web_contents) {
    web_contents->SetUserData(
        UserDataKey(),
        std::make_unique<TestMandatoryReauthBubbleControllerImpl>(
            web_contents));
  }

  explicit TestMandatoryReauthBubbleControllerImpl(
      content::WebContents* web_contents)
      : MandatoryReauthBubbleControllerImpl(web_contents) {}

  void DoShowBubble() override { SetBubbleView(dummy_bubble_); }

 private:
  TestAutofillBubble dummy_bubble_;
};

class MandatoryReauthBubbleControllerImplTest
    : public ChromeRenderViewHostTestHarness {
 public:
  MandatoryReauthBubbleControllerImplTest() = default;
  MandatoryReauthBubbleControllerImplTest(
      MandatoryReauthBubbleControllerImplTest&) = delete;
  MandatoryReauthBubbleControllerImplTest& operator=(
      MandatoryReauthBubbleControllerImplTest&) = delete;

  void SetUp() override {
    ChromeRenderViewHostTestHarness::SetUp();
    fake_tab_interface_ = std::make_unique<tabs::MockTabInterface>();
    tab_features_ = std::make_unique<tabs::TabFeatures>();
    auto mock_bubble_manager =
        std::make_unique<testing::NiceMock<autofill::MockBubbleManager>>();
    ON_CALL(*mock_bubble_manager, RequestShowController(testing::_, testing::_))
        .WillByDefault([](BubbleControllerBase& controller, bool force_show) {
          controller.ShowBubble();
        });
    tab_features_->SetBubbleManagerForTesting(std::move(mock_bubble_manager));

    ON_CALL(*fake_tab_interface_, GetContents())
        .WillByDefault(testing::Return(web_contents()));

    ON_CALL(*fake_tab_interface_, GetTabFeatures())
        .WillByDefault(testing::Return(tab_features_.get()));

    ON_CALL(testing::Const(*fake_tab_interface_), GetTabFeatures())
        .WillByDefault(testing::Return(
            static_cast<const tabs::TabFeatures*>(tab_features_.get())));

    tabs::TabLookupFromWebContents::CreateForWebContents(
        web_contents(), fake_tab_interface_.get());

    TestMandatoryReauthBubbleControllerImpl::CreateForTesting(web_contents());
  }

  void TearDown() override {
    if (web_contents()) {
      web_contents()->RemoveUserData(
          tabs::TabLookupFromWebContents::UserDataKey());
    }
    fake_tab_interface_.reset();
    ChromeRenderViewHostTestHarness::TearDown();
  }

  void ResetFakeTabInterface() { fake_tab_interface_.reset(); }

  void ShowBubble() {
    controller()->SetupAndShowBubble(
        accept_callback.Get(), cancel_callback.Get(), close_callback.Get());
  }

  void ReshowBubble() { controller()->ReshowBubble(); }

  void ClickAcceptButton() {
    controller()->OnBubbleClosed(PaymentsUiClosedReason::kAccepted);
  }

  void ClickCancelButton() {
    controller()->OnBubbleClosed(PaymentsUiClosedReason::kCancelled);
  }

  void CloseBubble() {
    controller()->OnBubbleClosed(PaymentsUiClosedReason::kClosed);
  }

  void LoseFocus() {
    controller()->OnBubbleClosed(PaymentsUiClosedReason::kLostFocus);
  }

  void FailToInteract() {
    controller()->OnBubbleClosed(PaymentsUiClosedReason::kNotInteracted);
  }

  base::MockOnceClosure accept_callback;
  base::MockOnceClosure cancel_callback;
  base::MockRepeatingClosure close_callback;

 protected:
  TestMandatoryReauthBubbleControllerImpl* controller() {
    return static_cast<TestMandatoryReauthBubbleControllerImpl*>(
        TestMandatoryReauthBubbleControllerImpl::FromWebContents(
            web_contents()));
  }

 private:
  std::unique_ptr<tabs::MockTabInterface> fake_tab_interface_;
  std::unique_ptr<tabs::TabFeatures> tab_features_;
  base::WeakPtrFactory<MandatoryReauthBubbleControllerImplTest>
      weak_ptr_factory_{this};
};

TEST_F(MandatoryReauthBubbleControllerImplTest,
       SuccessfullyInvokesAcceptCallback) {
  ShowBubble();
  EXPECT_CALL(accept_callback, Run).Times(1);
  ClickAcceptButton();
}

TEST_F(MandatoryReauthBubbleControllerImplTest,
       SuccessfullyInvokesCancelCallback) {
  ShowBubble();
  EXPECT_CALL(cancel_callback, Run).Times(1);
  ClickCancelButton();
}

TEST_F(MandatoryReauthBubbleControllerImplTest,
       SuccessfullyInvokesCloseCallback) {
  ShowBubble();
  EXPECT_CALL(close_callback, Run).Times(1);
  CloseBubble();
}

TEST_F(MandatoryReauthBubbleControllerImplTest,
       Metrics_OptInConfirmationBubble_Shown) {
  base::HistogramTester histogram_tester;
  ShowBubble();
  ClickAcceptButton();
  // Reshow the bubble, which has now transitioned to the confirmation state.
  ReshowBubble();

  histogram_tester.ExpectBucketCount(
      "Autofill.PaymentMethods.MandatoryReauth.OptInConfirmationBubble",
      autofill_metrics::MandatoryReauthOptInConfirmationBubbleMetric::kShown,
      1);
}

// Regression test: on macOS without biometrics, it's possible for the
// accept/cancel callbacks to destroy web contents when a user accepts the
// re-auth bubble, which would cause a use-after-free. This test ensures this
// case is handled.
TEST_F(MandatoryReauthBubbleControllerImplTest,
       OnBubbleClosedSurvivesWebContentsDestructionInAcceptCallback) {
  // The accept callback destroys the WebContents that owns `ctrl` (via
  // WebContentsUserData).
  base::OnceClosure destroy_web_contents = base::BindOnce(
      [](MandatoryReauthBubbleControllerImplTest* test) {
        if (test->web_contents()) {
          test->web_contents()->RemoveUserData(
              tabs::TabLookupFromWebContents::UserDataKey());
        }
        test->ResetFakeTabInterface();
        test->DeleteContents();
      },
      this);

  controller()->SetupAndShowBubble(std::move(destroy_web_contents),
                                   base::DoNothing(), base::DoNothing());

  // Simulate the user clicking "Yes" on the opt-in bubble.
  controller()->OnBubbleClosed(PaymentsUiClosedReason::kAccepted);
}

class MandatoryReauthBubbleControllerOptInBubbleMetricsTest
    : public MandatoryReauthBubbleControllerImplTest,
      public testing::WithParamInterface<bool> {
 public:
  bool IsReshow() { return GetParam(); }

  void SetUpMetricsTest() {
    ShowBubble();
    if (IsReshow()) {
      CloseBubble();
      ReshowBubble();
    }
  }
};

std::string GetOfferMetricsPath(bool is_reshow) {
  return base::StrCat(
      {"Autofill.PaymentMethods.MandatoryReauth.OptInBubbleOffer.",
       is_reshow ? "Reshow" : "FirstShow"});
}

std::string GetResultMetricsPath(bool is_reshow) {
  return base::StrCat(
      {"Autofill.PaymentMethods.MandatoryReauth.OptInBubbleResult.",
       is_reshow ? "Reshow" : "FirstShow"});
}

INSTANTIATE_TEST_SUITE_P(FirstShowAndReshow,
                         MandatoryReauthBubbleControllerOptInBubbleMetricsTest,
                         testing::Bool());

TEST_P(MandatoryReauthBubbleControllerOptInBubbleMetricsTest,
       Metrics_OptInBubble_Shown) {
  base::HistogramTester histogram_tester;
  SetUpMetricsTest();

  histogram_tester.ExpectUniqueSample(
      GetOfferMetricsPath(/*is_reshow=*/false),
      autofill_metrics::MandatoryReauthOptInBubbleOffer::kShown, 1);
  if (IsReshow()) {
    histogram_tester.ExpectUniqueSample(
        GetOfferMetricsPath(/*is_reshow=*/true),
        autofill_metrics::MandatoryReauthOptInBubbleOffer::kShown, 1);
  }
}

TEST_P(MandatoryReauthBubbleControllerOptInBubbleMetricsTest,
       Metrics_MandatoryReauthOptInBubbleMetric_Accepted) {
  base::HistogramTester histogram_tester;
  SetUpMetricsTest();
  ClickAcceptButton();

  histogram_tester.ExpectBucketCount(
      GetResultMetricsPath(IsReshow()),
      autofill_metrics::MandatoryReauthOptInBubbleResult::kAccepted, 1);
}

TEST_P(MandatoryReauthBubbleControllerOptInBubbleMetricsTest,
       Metrics_OptInBubble_Cancelled) {
  base::HistogramTester histogram_tester;
  SetUpMetricsTest();
  ClickCancelButton();

  histogram_tester.ExpectBucketCount(
      GetResultMetricsPath(IsReshow()),
      autofill_metrics::MandatoryReauthOptInBubbleResult::kCancelled, 1);
}

TEST_P(MandatoryReauthBubbleControllerOptInBubbleMetricsTest,
       Metrics_OptInBubble_Closed) {
  base::HistogramTester histogram_tester;
  SetUpMetricsTest();
  CloseBubble();

  histogram_tester.ExpectBucketCount(
      GetResultMetricsPath(IsReshow()),
      autofill_metrics::MandatoryReauthOptInBubbleResult::kClosed, 1);
}

TEST_P(MandatoryReauthBubbleControllerOptInBubbleMetricsTest,
       Metrics_OptInBubble_LostFocus) {
  base::HistogramTester histogram_tester;
  SetUpMetricsTest();
  LoseFocus();

  histogram_tester.ExpectBucketCount(
      GetResultMetricsPath(IsReshow()),
      autofill_metrics::MandatoryReauthOptInBubbleResult::kLostFocus, 1);
}

TEST_P(MandatoryReauthBubbleControllerOptInBubbleMetricsTest,
       MetricsOptInBubble_NotInteracted) {
  base::HistogramTester histogram_tester;
  SetUpMetricsTest();
  FailToInteract();

  histogram_tester.ExpectBucketCount(
      GetResultMetricsPath(IsReshow()),
      autofill_metrics::MandatoryReauthOptInBubbleResult::kNotInteracted, 1);
}

}  // namespace autofill
