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

#include "ash/system/mahi/mahi_ui_controller.h"

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

#include "ash/public/cpp/session/session_types.h"
#include "ash/session/session_controller_impl.h"
#include "ash/session/test_session_controller_client.h"
#include "ash/shell.h"
#include "ash/system/mahi/mahi_constants.h"
#include "ash/system/mahi/mahi_panel_view.h"
#include "ash/system/mahi/mahi_ui_update.h"
#include "ash/system/mahi/test/mahi_test_util.h"
#include "ash/system/mahi/test/mock_mahi_manager.h"
#include "ash/system/mahi/test/mock_mahi_ui_controller_delegate.h"
#include "ash/test/ash_test_base.h"
#include "base/task/current_thread.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_future.h"
#include "base/time/time.h"
#include "chromeos/constants/chromeos_features.h"
#include "components/session_manager/session_manager_types.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/gfx/geometry/rect.h"
#include "ui/views/view.h"

namespace ash {

namespace {

// Aliases ---------------------------------------------------------------------

using chromeos::MahiResponseStatus;
using ::testing::AllOf;
using ::testing::ElementsAreArray;
using ::testing::Eq;
using ::testing::Field;
using ::testing::InSequence;
using ::testing::Mock;
using ::testing::NiceMock;
using ::testing::Property;
using ::testing::Return;

// MockView --------------------------------------------------------------------

class MockView : public views::View {
 public:
  MOCK_METHOD(void, VisibilityChanged, (views::View*, bool), (override));
};

}  // namespace

class MahiUiControllerTest : public AshTestBase {
 public:
  MahiUiControllerTest()
      : AshTestBase(base::test::TaskEnvironment::TimeSource::MOCK_TIME) {}

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

  ~MahiUiControllerTest() override = default;

 protected:
  MockMahiUiControllerDelegate& delegate() { return delegate_; }
  MockView& delegate_view() { return delegate_view_; }
  MockMahiManager& mock_mahi_manager() { return mock_mahi_manager_; }
  MahiUiController& ui_controller() { return ui_controller_; }

 private:
  // AshTestBase:
  void SetUp() override {
    scoped_feature_list_.InitWithFeatures(
        /*enabled_features=*/{chromeos::features::kFeatureManagementMahi},
        /*disabled_features=*/{});

    ON_CALL(mock_mahi_manager_, IsEnabled).WillByDefault(Return(true));
    ON_CALL(delegate_, GetView).WillByDefault(Return(&delegate_view_));
    AshTestBase::SetUp();
    scoped_setter_ = std::make_unique<chromeos::ScopedMahiManagerSetter>(
        &mock_mahi_manager_);
  }

  void TearDown() override {
    scoped_setter_.reset();
    AshTestBase::TearDown();
  }

  base::test::ScopedFeatureList scoped_feature_list_;
  NiceMock<MockView> delegate_view_;
  NiceMock<MahiUiController> ui_controller_;
  NiceMock<MockMahiUiControllerDelegate> delegate_{&ui_controller_};
  NiceMock<MockMahiManager> mock_mahi_manager_;
  std::unique_ptr<chromeos::ScopedMahiManagerSetter> scoped_setter_;
};

// Checks `MahiUiController::Delegate` when navigating to the state that the
// view displaying questions and answers should show.
TEST_F(MahiUiControllerTest, NavigateToQuestionAnswerView) {
  EXPECT_CALL(delegate(),
              OnUpdated(Property(
                  &MahiUiUpdate::type,
                  Eq(MahiUiUpdateType::kQuestionAndAnswerViewNavigated))));
  EXPECT_CALL(delegate_view(),
              VisibilityChanged(&delegate_view(), /*is_visible=*/false));
  ui_controller().NavigateToQuestionAnswerView();
  Mock::VerifyAndClearExpectations(&delegate());
  Mock::VerifyAndClearExpectations(&delegate_view());

  // Config the delegate's visibility state and expect the delegate view to be
  // visible after navigation.
  ON_CALL(delegate(), GetViewVisibility)
      .WillByDefault([](VisibilityState state) {
        return state == VisibilityState::kQuestionAndAnswer;
      });

  EXPECT_CALL(delegate_view(),
              VisibilityChanged(&delegate_view(), /*is_visible=*/true));
  EXPECT_CALL(delegate(),
              OnUpdated(Property(
                  &MahiUiUpdate::type,
                  Eq(MahiUiUpdateType::kQuestionAndAnswerViewNavigated))));
  ui_controller().NavigateToQuestionAnswerView();
  Mock::VerifyAndClearExpectations(&delegate());
  Mock::VerifyAndClearExpectations(&delegate_view());
}

// Checks `MahiUiController::Delegate` when navigating to the state that the
// view displaying summary and outlines should show.
TEST_F(MahiUiControllerTest, NavigateToSummaryOutlinesSection) {
  EXPECT_CALL(delegate(),
              OnUpdated(Property(
                  &MahiUiUpdate::type,
                  Eq(MahiUiUpdateType::kSummaryAndOutlinesSectionNavigated))));
  EXPECT_CALL(delegate_view(),
              VisibilityChanged(&delegate_view(), /*is_visible=*/false));
  ui_controller().NavigateToSummaryOutlinesSection();
  Mock::VerifyAndClearExpectations(&delegate());
  Mock::VerifyAndClearExpectations(&delegate_view());

  // Config the delegate's visibility state and expect the delegate view to be
  // visible after navigation.
  ON_CALL(delegate(), GetViewVisibility)
      .WillByDefault([](VisibilityState state) {
        return state == VisibilityState::kSummaryAndOutlinesAndElucidation;
      });

  EXPECT_CALL(delegate_view(),
              VisibilityChanged(&delegate_view(), /*is_visible=*/true));
  EXPECT_CALL(delegate(),
              OnUpdated(Property(
                  &MahiUiUpdate::type,
                  Eq(MahiUiUpdateType::kSummaryAndOutlinesSectionNavigated))));
  ui_controller().NavigateToSummaryOutlinesSection();
  Mock::VerifyAndClearExpectations(&delegate());
  Mock::VerifyAndClearExpectations(&delegate_view());
}

// Checks `MahiUiController::Delegate` when the refresh availability updates.
TEST_F(MahiUiControllerTest, NotifyRefreshAvailabilityChanged) {
  // Check when the refresh availability becomes false.
  EXPECT_CALL(delegate(),
              OnUpdated(AllOf(
                  Property(&MahiUiUpdate::type,
                           Eq(MahiUiUpdateType::kRefreshAvailabilityUpdated)),
                  Property(&MahiUiUpdate::GetRefreshAvailability, false))));
  ui_controller().NotifyRefreshAvailabilityChanged(/*available=*/false);
  Mock::VerifyAndClearExpectations(&delegate());

  // Check when the refresh availability becomes true.
  EXPECT_CALL(delegate(),
              OnUpdated(AllOf(
                  Property(&MahiUiUpdate::type,
                           Eq(MahiUiUpdateType::kRefreshAvailabilityUpdated)),
                  Property(&MahiUiUpdate::GetRefreshAvailability, true))));
  ui_controller().NotifyRefreshAvailabilityChanged(/*available=*/true);
  Mock::VerifyAndClearExpectations(&delegate());
}

// Checks `MahiUiController::Delegate` when the panel bounds change.
TEST_F(MahiUiControllerTest, NotifyPanelBoundsChanged) {
  const gfx::Rect expected_bounds = gfx::Rect(80, 80);
  EXPECT_CALL(delegate(),
              OnUpdated(AllOf(
                  Property(&MahiUiUpdate::type,
                           Eq(MahiUiUpdateType::kPanelBoundsChanged)),
                  Property(&MahiUiUpdate::GetPanelBounds, expected_bounds))));
  ui_controller().NotifyPanelBoundsChanged(expected_bounds);
  Mock::VerifyAndClearExpectations(&delegate());
}

// Checks `MahiUiController::Delegate` when the contents get refreshed.
TEST_F(MahiUiControllerTest, RefreshContents) {
  InSequence s;
  EXPECT_CALL(delegate(),
              OnUpdated(Property(
                  &MahiUiUpdate::type,
                  Eq(MahiUiUpdateType::kSummaryAndOutlinesSectionNavigated))));
  EXPECT_CALL(
      delegate(),
      OnUpdated(Property(&MahiUiUpdate::type,
                         Eq(MahiUiUpdateType::kContentsRefreshInitiated))));

  ui_controller().RefreshContents();
  Mock::VerifyAndClearExpectations(&delegate());
}

// Checks `MahiUiController::Delegate` when the contents get refreshed and the
// panel is for elucidation purpose.
TEST_F(MahiUiControllerTest, RefreshContentsForElucidation) {
  // Calls `OpenMahiPanel` to set `elucidation_in_use_` to true.
  // This creates a panel widget and implicitly triggers `RefreshContents`
  // and `PanelBoundsChanged` calls, hence the first sequence.
  // Because `kMahiPanelResizable` is enabled by default, there are two
  // `PanelBoundsChanged` calls.
  {
    InSequence s;
    EXPECT_CALL(
        delegate(),
        OnUpdated(Property(
            &MahiUiUpdate::type,
            Eq(MahiUiUpdateType::kSummaryAndOutlinesSectionNavigated))));
    EXPECT_CALL(delegate(), OnUpdated(Property(
                                &MahiUiUpdate::type,
                                Eq(MahiUiUpdateType::kElucidationRequested))));
    EXPECT_CALL(delegate(),
                OnUpdated(Property(&MahiUiUpdate::type,
                                   Eq(MahiUiUpdateType::kPanelBoundsChanged))))
        .Times(2);
  }

  ui_controller().OpenMahiPanel(GetPrimaryDisplay().id(), gfx::Rect(),
                                /*elucidation_in_use=*/true);
  Mock::VerifyAndClearExpectations(&delegate());

  // Manually calls `RefreshContents` with `elucidation_in_use_` = true, it
  // causes the sequence again.
  {
    InSequence s;
    EXPECT_CALL(
        delegate(),
        OnUpdated(Property(
            &MahiUiUpdate::type,
            Eq(MahiUiUpdateType::kSummaryAndOutlinesSectionNavigated))));
    EXPECT_CALL(delegate(), OnUpdated(Property(
                                &MahiUiUpdate::type,
                                Eq(MahiUiUpdateType::kElucidationRequested))));
  }
  ui_controller().RefreshContents();
  Mock::VerifyAndClearExpectations(&delegate());
}

// Checks `MahiUiController::Delegate` when retrying summary and outlines.
TEST_F(MahiUiControllerTest, RetrySummaryAndOutlines) {
  EXPECT_CALL(
      delegate(),
      OnUpdated(Property(&MahiUiUpdate::type,
                         Eq(MahiUiUpdateType::kSummaryAndOutlinesReloaded))));

  ui_controller().Retry(VisibilityState::kSummaryAndOutlinesAndElucidation);
  Mock::VerifyAndClearExpectations(&delegate());
}

// Checks `MahiUiController::Delegate` when retrying the previous question.
TEST_F(MahiUiControllerTest, RetrySendQuestion) {
  // Send a question before retrying to ensure a previous question is available.
  const std::u16string question(u"fake question");
  const bool current_panel_content = true;
  ui_controller().SendQuestion(question, current_panel_content,
                               MahiUiController::QuestionSource::kPanel);

  EXPECT_CALL(
      delegate(),
      OnUpdated(AllOf(
          Property(&MahiUiUpdate::type, Eq(MahiUiUpdateType::kQuestionReAsked)),
          Property(&MahiUiUpdate::GetReAskQuestionParams,
                   AllOf(Field(&MahiQuestionParams::current_panel_content,
                               current_panel_content),
                         Field(&MahiQuestionParams::question, question))))));

  ui_controller().Retry(VisibilityState::kQuestionAndAnswer);
  Mock::VerifyAndClearExpectations(&delegate());
}

// Checks `MahiUiController::Delegate` when sending a question.
TEST_F(MahiUiControllerTest, SendQuestion) {
  const std::u16string answer(u"fake answer");
  ON_CALL(mock_mahi_manager(), AnswerQuestion)
      .WillByDefault(
          [&answer](
              const std::u16string& question, bool current_panel_content,
              chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
            std::move(callback).Run(answer, MahiResponseStatus::kSuccess);
          });

  const std::u16string summary(u"fake summary");
  ON_CALL(mock_mahi_manager(), GetSummary)
      .WillByDefault(
          [&summary](chromeos::MahiManager::MahiSummaryCallback callback) {
            std::move(callback).Run(summary, MahiResponseStatus::kSuccess);
          });

  InSequence s;
  const std::u16string question(u"fake question");
  EXPECT_CALL(delegate(),
              OnUpdated(AllOf(Property(&MahiUiUpdate::type,
                                       Eq(MahiUiUpdateType::kQuestionPosted)),
                              Property(&MahiUiUpdate::GetQuestion, question))));
  EXPECT_CALL(delegate(),
              OnUpdated(AllOf(Property(&MahiUiUpdate::type,
                                       Eq(MahiUiUpdateType::kAnswerLoaded)),
                              Property(&MahiUiUpdate::GetAnswer, answer))));

  // `kSummaryLoaded` update should not be called when
  // `update_summary_after_answer_question` is false.
  EXPECT_CALL(delegate(),
              OnUpdated(Property(&MahiUiUpdate::type,
                                 Eq(MahiUiUpdateType::kSummaryLoaded))))
      .Times(0);

  ui_controller().SendQuestion(question, /*current_panel_content=*/true,
                               MahiUiController::QuestionSource::kPanel,
                               /*update_summary_after_answer_question=*/false);
  Mock::VerifyAndClearExpectations(&delegate());

  EXPECT_CALL(delegate(),
              OnUpdated(AllOf(Property(&MahiUiUpdate::type,
                                       Eq(MahiUiUpdateType::kQuestionPosted)),
                              Property(&MahiUiUpdate::GetQuestion, question))));
  EXPECT_CALL(delegate(),
              OnUpdated(AllOf(Property(&MahiUiUpdate::type,
                                       Eq(MahiUiUpdateType::kAnswerLoaded)),
                              Property(&MahiUiUpdate::GetAnswer, answer))));

  // `kSummaryLoaded` update should be called when
  // `update_summary_after_answer_question` is true.
  EXPECT_CALL(delegate(),
              OnUpdated(AllOf(Property(&MahiUiUpdate::type,
                                       Eq(MahiUiUpdateType::kSummaryLoaded)),
                              Property(&MahiUiUpdate::GetSummary, summary))));

  ui_controller().SendQuestion(question, /*current_panel_content=*/true,
                               MahiUiController::QuestionSource::kPanel,
                               /*update_summary_after_answer_question=*/true);
  Mock::VerifyAndClearExpectations(&delegate());
}

// Checks `MahiUiController::Delegate` when the summary and outlines update.
TEST_F(MahiUiControllerTest, UpdateSummaryAndOutlines) {
  // Config the mock mahi manager to return summary and outlines.
  const std::u16string summary(u"fake summary");
  ON_CALL(mock_mahi_manager(), GetSummary)
      .WillByDefault(
          [&summary](chromeos::MahiManager::MahiSummaryCallback callback) {
            std::move(callback).Run(summary, MahiResponseStatus::kSuccess);
          });
  ON_CALL(mock_mahi_manager(), GetOutlines)
      .WillByDefault(mahi_test_util::ReturnDefaultOutlines);

  EXPECT_CALL(delegate(),
              OnUpdated(AllOf(Property(&MahiUiUpdate::type,
                                       Eq(MahiUiUpdateType::kSummaryLoaded)),
                              Property(&MahiUiUpdate::GetSummary, summary))));
  EXPECT_CALL(
      delegate(),
      OnUpdated(AllOf(
          Property(&MahiUiUpdate::type, Eq(MahiUiUpdateType::kOutlinesLoaded)),
          Property(&MahiUiUpdate::GetOutlines,
                   testing::ElementsAreArray(
                       mahi_test_util::GetDefaultFakeOutlines())))));

  ui_controller().UpdateSummaryAndOutlines();
  Mock::VerifyAndClearExpectations(&delegate());
}

// Checks new requests can discard pending ones to avoid racing.
TEST_F(MahiUiControllerTest, RacingRequests) {
  // Configs the mock mahi manager to respond async-ly.
  base::test::TestFuture<void> summary_waiter;
  ON_CALL(mock_mahi_manager(), GetSummary)
      .WillByDefault([&summary_waiter](
                         chromeos::MahiManager::MahiSummaryCallback callback) {
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](base::OnceClosure unblock_closure,
                   chromeos::MahiManager::MahiSummaryCallback callback) {
                  std::move(callback).Run(u"fake summary",
                                          MahiResponseStatus::kSuccess);
                  std::move(unblock_closure).Run();
                },
                summary_waiter.GetCallback(), std::move(callback)));
      });

  base::test::TestFuture<void> outline_waiter;
  ON_CALL(mock_mahi_manager(), GetOutlines)
      .WillByDefault([&outline_waiter](
                         chromeos::MahiManager::MahiOutlinesCallback callback) {
        base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](base::OnceClosure unblock_closure,
                   chromeos::MahiManager::MahiOutlinesCallback callback) {
                  mahi_test_util::ReturnDefaultOutlines(std::move(callback));
                  std::move(unblock_closure).Run();
                },
                outline_waiter.GetCallback(), std::move(callback)));
      });

  base::test::TestFuture<void> answer_waiter;
  ON_CALL(mock_mahi_manager(), AnswerQuestion)
      .WillByDefault([&answer_waiter](
                         const std::u16string& question,
                         bool current_panel_content,
                         chromeos::MahiManager::MahiAnswerQuestionCallback
                             callback) {
        base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE,
            base::BindOnce(
                [](base::OnceClosure unblock_closure,
                   chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
                  std::move(callback).Run(u"fake answer",
                                          MahiResponseStatus::kSuccess);
                  std::move(unblock_closure).Run();
                },
                answer_waiter.GetCallback(), std::move(callback)));
      });

  // Pending `UpdateSummaryAndOutlines` is discarded on new `SendQuestion` call.
  // So that `OnUpdated` is only called with QA types.
  EXPECT_CALL(delegate(),
              OnUpdated(Property(&MahiUiUpdate::type,
                                 Eq(MahiUiUpdateType::kQuestionPosted))));
  EXPECT_CALL(delegate(),
              OnUpdated(Property(&MahiUiUpdate::type,
                                 Eq(MahiUiUpdateType::kAnswerLoaded))));

  EXPECT_CALL(delegate(),
              OnUpdated(Property(&MahiUiUpdate::type,
                                 Eq(MahiUiUpdateType::kSummaryLoaded))))
      .Times(0);
  EXPECT_CALL(delegate(),
              OnUpdated(Property(&MahiUiUpdate::type,
                                 Eq(MahiUiUpdateType::kOutlinesLoaded))))
      .Times(0);

  ui_controller().UpdateSummaryAndOutlines();
  ui_controller().SendQuestion(u"fake question", /*current_panel_content=*/true,
                               MahiUiController::QuestionSource::kPanel);

  ASSERT_TRUE(outline_waiter.Wait());
  ASSERT_TRUE(summary_waiter.Wait());
  ASSERT_TRUE(answer_waiter.Wait());
  Mock::VerifyAndClearExpectations(&delegate());
}

// Tests to make sure that when navigating back to summary view, the view is not
// stuck at loading (b/345621992).
TEST_F(MahiUiControllerTest, UpdateSummaryAfterAnswerQuestionAsync) {
  auto delay_time = base::Milliseconds(10);
  // Configs the mock mahi manager to respond async-ly.
  ON_CALL(mock_mahi_manager(), GetSummary)
      .WillByDefault(
          [delay_time](chromeos::MahiManager::MahiSummaryCallback callback) {
            base::SequencedTaskRunner::GetCurrentDefault()->PostDelayedTask(
                FROM_HERE,
                base::BindOnce(
                    [](chromeos::MahiManager::MahiSummaryCallback callback) {
                      std::move(callback).Run(u"fake summary",
                                              MahiResponseStatus::kSuccess);
                    },
                    std::move(callback)),
                delay_time);
          });

  ON_CALL(mock_mahi_manager(), AnswerQuestion)
      .WillByDefault([delay_time](
                         const std::u16string& question,
                         bool current_panel_content,
                         chromeos::MahiManager::MahiAnswerQuestionCallback
                             callback) {
        base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
            FROM_HERE,
            base::BindOnce(
                [](chromeos::MahiManager::MahiAnswerQuestionCallback callback) {
                  std::move(callback).Run(u"fake answer",
                                          MahiResponseStatus::kSuccess);
                },
                std::move(callback)),
            delay_time);
      });

  MahiPanelView panel_view(&ui_controller());
  ui_controller().SendQuestion(u"fake question", /*current_panel_content=*/true,
                               MahiUiController::QuestionSource::kMenuView,
                               /*update_summary_after_answer_question=*/true);

  auto* question_answer_view =
      panel_view.GetViewByID(mahi_constants::ViewId::kQuestionAnswerView);
  auto* summary_outlines_section =
      panel_view.GetViewByID(mahi_constants::ViewId::kSummaryOutlinesSection);

  EXPECT_TRUE(question_answer_view->GetVisible());
  EXPECT_FALSE(summary_outlines_section->GetVisible());

  ui_controller().NavigateToSummaryOutlinesSection();
  EXPECT_FALSE(question_answer_view->GetVisible());
  EXPECT_TRUE(summary_outlines_section->GetVisible());

  // Summary is loading after answering a question.
  EXPECT_TRUE(
      panel_view
          .GetViewByID(mahi_constants::ViewId::kSummaryLoadingAnimatedImage)
          ->GetVisible());
  EXPECT_FALSE(panel_view.GetViewByID(mahi_constants::ViewId::kSummaryLabel)
                   ->GetVisible());

  // Fast forward until summary is loaded.
  task_environment()->FastForwardBy(base::Milliseconds(30));

  // The loading image should not be visible now since summary is fully loaded.
  EXPECT_FALSE(
      panel_view
          .GetViewByID(mahi_constants::ViewId::kSummaryLoadingAnimatedImage)
          ->GetVisible());
  EXPECT_TRUE(panel_view.GetViewByID(mahi_constants::ViewId::kSummaryLabel)
                  ->GetVisible());
}

// Test suite where the UI controller and its delegate are initialized on
// `SetUp` so ash::Shell can be initialized and the session state observed.
class MahiUiControllerWithSessionTest : public AshTestBase {
 protected:
  MockView& delegate_view() { return delegate_view_; }
  MockMahiManager& mock_mahi_manager() { return mock_mahi_manager_; }

  MockMahiUiControllerDelegate* delegate() { return delegate_.get(); }
  MahiUiController* ui_controller() { return ui_controller_.get(); }

 private:
  // AshTestBase:
  void SetUp() override {
    AshTestBase::SetUp();
    scoped_feature_list_.InitWithFeatures(
        {chromeos::features::kFeatureManagementMahi}, {});
    ui_controller_ = std::make_unique<NiceMock<MahiUiController>>();
    delegate_ = std::make_unique<NiceMock<MockMahiUiControllerDelegate>>(
        ui_controller_.get());
    scoped_setter_ = std::make_unique<chromeos::ScopedMahiManagerSetter>(
        &mock_mahi_manager_);

    ON_CALL(mock_mahi_manager_, IsEnabled).WillByDefault(Return(true));
    ON_CALL(*delegate(), GetView).WillByDefault(Return(&delegate_view_));
  }

  void TearDown() override {
    AshTestBase::TearDown();
    delegate_.reset();
    ui_controller_.reset();
    scoped_setter_.reset();
  }

  NiceMock<MockView> delegate_view_;
  NiceMock<MockMahiManager> mock_mahi_manager_;
  std::unique_ptr<NiceMock<MahiUiController>> ui_controller_;
  std::unique_ptr<NiceMock<MockMahiUiControllerDelegate>> delegate_;
  base::test::ScopedFeatureList scoped_feature_list_;
  std::unique_ptr<chromeos::ScopedMahiManagerSetter> scoped_setter_;
};

// Tests that the `TimesPanelOpenedPerSession` metric records the amount of
// times the panel was opened and emits when the screen is locked.
TEST_F(MahiUiControllerWithSessionTest, TimesPanelOpenedPerSessionMetric) {
  base::HistogramTester histogram_tester;
  const int kTimesPanelWillOpen = 3;

  // Test that locking the screen will record the amount of times the panel
  // was opened while the session was active.
  for (int i = 0; i < kTimesPanelWillOpen; i++) {
    ui_controller()->OpenMahiPanel(GetPrimaryDisplay().id(), gfx::Rect(),
                                   /*elucidation_in_use=*/false);
    // Immediately close the widget to avoid a dangling pointer.
    ui_controller()->mahi_panel_widget()->CloseNow();
  }
  histogram_tester.ExpectBucketCount(
      mahi_constants::kTimesMahiPanelOpenedPerSessionHistogramName,
      /*sample=*/kTimesPanelWillOpen, /*expected_count=*/0);
  GetSessionControllerClient()->LockScreen();
  histogram_tester.ExpectBucketCount(
      mahi_constants::kTimesMahiPanelOpenedPerSessionHistogramName,
      /*sample=*/kTimesPanelWillOpen, /*expected_count=*/1);

  // Test that the metric does not get recorded if the panel was never opened.
  GetSessionControllerClient()->UnlockScreen();
  GetSessionControllerClient()->LockScreen();
  histogram_tester.ExpectBucketCount(
      mahi_constants::kTimesMahiPanelOpenedPerSessionHistogramName,
      /*sample=*/0, /*expected_count=*/0);
}

// Tests that the metric is recorded after a state change from `ACTIVE` to
// any other session state.
TEST_F(MahiUiControllerWithSessionTest,
       TimesPanelOpenedPerSessionMetric_AllSessionStates) {
  base::HistogramTester histogram_tester;

  std::vector<session_manager::SessionState> non_active_session_states = {
      session_manager::SessionState::UNKNOWN,
      session_manager::SessionState::OOBE,
      session_manager::SessionState::LOGIN_PRIMARY,
      session_manager::SessionState::LOGGED_IN_NOT_ACTIVE,
      session_manager::SessionState::LOCKED,
      session_manager::SessionState::LOGIN_SECONDARY,
      session_manager::SessionState::RMA};

  SessionInfo info;
  for (size_t i = 0; i < non_active_session_states.size(); i++) {
    // Set the session to active so the count to be recorded can be accumulated.
    info.state = session_manager::SessionState::ACTIVE;
    Shell::Get()->session_controller()->SetSessionInfo(info);

    // Open and close the mahi panel. The metric should not be recorded yet.
    ui_controller()->OpenMahiPanel(GetPrimaryDisplay().id(), gfx::Rect(),
                                   /*elucidation_in_use=*/false);
    // Immediately close the widget to avoid a dangling pointer.
    ui_controller()->mahi_panel_widget()->CloseNow();
    histogram_tester.ExpectBucketCount(
        mahi_constants::kTimesMahiPanelOpenedPerSessionHistogramName,
        /*sample=*/1, /*expected_count=*/i);

    // Set the session to a non-active state. The metric should be recorded.
    info.state = non_active_session_states[i];
    Shell::Get()->session_controller()->SetSessionInfo(info);
    histogram_tester.ExpectBucketCount(
        mahi_constants::kTimesMahiPanelOpenedPerSessionHistogramName,
        /*sample=*/1, /*expected_count=*/i + 1);
  }
}

TEST_F(MahiUiControllerWithSessionTest, PanelCloseOnSessionStateChanged) {
  std::vector<session_manager::SessionState> non_active_session_states = {
      session_manager::SessionState::UNKNOWN,
      session_manager::SessionState::OOBE,
      session_manager::SessionState::LOGIN_PRIMARY,
      session_manager::SessionState::LOGGED_IN_NOT_ACTIVE,
      session_manager::SessionState::LOCKED,
      session_manager::SessionState::LOGIN_SECONDARY,
      session_manager::SessionState::RMA};

  SessionInfo info;
  for (size_t i = 0; i < non_active_session_states.size(); i++) {
    info.state = session_manager::SessionState::ACTIVE;
    Shell::Get()->session_controller()->SetSessionInfo(info);

    ui_controller()->OpenMahiPanel(GetPrimaryDisplay().id(), gfx::Rect(),
                                   /*elucidation_in_use=*/false);
    EXPECT_TRUE(ui_controller()->IsMahiPanelOpen());

    // Set the session to a non-active state, the panel should be closed.
    auto state = non_active_session_states[i];
    info.state = state;
    Shell::Get()->session_controller()->SetSessionInfo(info);
    base::test::RunUntil([&state] {
      return Shell::Get()->session_controller()->GetSessionState() == state;
    });
    EXPECT_FALSE(ui_controller()->IsMahiPanelOpen());
  }
}

class MahiUiControllerWithNoSessionTest
    : public MahiUiControllerWithSessionTest {
 public:
  MahiUiControllerWithNoSessionTest() { set_start_session(false); }

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

  ~MahiUiControllerWithNoSessionTest() override = default;
};

TEST_F(MahiUiControllerWithNoSessionTest, PanelCloseOnActiveUserChanged) {
  // Set up two users, user1 is the active user.
  AccountId account_id1 = AccountId::FromUserEmail("user1@test.com");
  SimulateUserLogin(account_id1);
  AccountId account_id2 = AccountId::FromUserEmail("user2@test.com");
  SimulateUserLogin(account_id2);
  SwitchActiveUser(account_id1);
  ASSERT_TRUE(Shell::Get()->session_controller()->IsUserPrimary());

  ui_controller()->OpenMahiPanel(GetPrimaryDisplay().id(), gfx::Rect(),
                                 /*elucidation_in_use=*/false);
  EXPECT_TRUE(ui_controller()->IsMahiPanelOpen());

  // Make user2 the active user, the panel should be closed.
  SwitchActiveUser(account_id2);
  ASSERT_FALSE(Shell::Get()->session_controller()->IsUserPrimary());
  EXPECT_FALSE(ui_controller()->IsMahiPanelOpen());
}

}  // namespace ash
