// Copyright 2025 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/actor/actor_keyed_service.h"

#include <memory>
#include <optional>

#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "chrome/browser/actor/actor_task.h"
#include "chrome/browser/actor/actor_test_util.h"
#include "chrome/browser/actor/enterprise_policy_checker.h"
#include "chrome/browser/actor/execution_engine.h"
#include "chrome/browser/actor/ui/event_dispatcher.h"
#include "chrome/browser/actor/ui/test_support/mock_actor_ui_state_manager.h"
#include "chrome/common/actor/action_result.h"
#include "chrome/common/chrome_features.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "components/actor/core/actor_features.h"
#include "components/actor/core/task_source_info.h"
#include "components/actor/public/mojom/actor_types.mojom.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace actor {

namespace {

using ::testing::_;

std::unique_ptr<ui::ActorUiStateManagerInterface> BuildUiStateManagerMock() {
  std::unique_ptr<ui::MockActorUiStateManager> ui_state_manager =
      std::make_unique<ui::MockActorUiStateManager>();
  ON_CALL(*ui_state_manager, OnUiEvent(_, _))
      .WillByDefault([](ui::AsyncUiEvent, ui::UiCompleteCallback callback) {
        std::move(callback).Run(MakeOkResult());
      });
  return ui_state_manager;
}

class ActorKeyedServiceTest : public testing::Test {
 public:
  ActorKeyedServiceTest()
      : task_environment_(base::test::TaskEnvironment::TimeSource::MOCK_TIME),
        testing_profile_manager_(TestingBrowserProcess::GetGlobal()) {
    scoped_feature_list_.InitAndEnableFeature(features::kGlicActor);
  }
  ~ActorKeyedServiceTest() override = default;

  // testing::Test:
  void SetUp() override {
    ASSERT_TRUE(testing_profile_manager_.SetUp());
    profile_ = testing_profile_manager()->CreateTestingProfile("profile");
    auto* actor_service = ActorKeyedService::Get(profile());
    ASSERT_TRUE(actor_service);
    actor_service->SetActorUiStateManagerForTesting(BuildUiStateManagerMock());
  }

  TestingProfileManager* testing_profile_manager() {
    return &testing_profile_manager_;
  }

  TestingProfile* profile() { return profile_.get(); }

 protected:
  base::CallbackListSubscription user_confirmation_dialog_subscription_;
  base::CallbackListSubscription confirm_navigation_subscription_;

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  content::BrowserTaskEnvironment task_environment_;
  TestingProfileManager testing_profile_manager_;
  raw_ptr<TestingProfile> profile_;
};

// Adds a task to ActorKeyedService
TEST_F(ActorKeyedServiceTest, AddActiveTask) {
  auto* actor_service = ActorKeyedService::Get(profile());
  actor_service->CreateTask(TestTaskSourceInfo(), NoEnterprisePolicyChecker());
  ASSERT_EQ(actor_service->GetActiveTasks().size(), 1u);
  EXPECT_EQ(actor_service->GetActiveTasks().begin()->second->GetState(),
            ActorTask::State::kCreated);
}

// Stops a task.
TEST_F(ActorKeyedServiceTest, StopActiveTask) {
  auto* actor_service = ActorKeyedService::Get(profile());
  TaskId id = actor_service->CreateTask(TestTaskSourceInfo(),
                                        NoEnterprisePolicyChecker());

  // Add a tab to the task
  base::WeakPtr<ActorTask> task = actor_service->GetTask(id)->GetWeakPtr();
  base::RunLoop loop;
  task->AddTab(tabs::TabHandle(123),
               /*stop_task_on_detach=*/true,
               base::BindLambdaForTesting([&](mojom::ActionResultPtr result) {
                 EXPECT_TRUE(IsOk(*result));
                 loop.Quit();
               }));
  loop.Run();

  EXPECT_TRUE(task->IsActingOnTab(tabs::TabHandle(123)));
  EXPECT_TRUE(task->HasTab(tabs::TabHandle(123)));
  actor_service->StopTask(id, ActorTask::StoppedReason::kTaskComplete);

  // Tasks are deleted asynchronously.
  EXPECT_TRUE(task);
  EXPECT_EQ(task->GetState(), ActorTask::State::kFinished);

  // Ensure the task is eventually deleted.
  WaitForPostedTask();
  ASSERT_EQ(actor_service->GetActiveTasks().size(), 0u);
  ASSERT_FALSE(task);
}

TEST_F(ActorKeyedServiceTest, FindTaskIdsInActive_ReturnsSuccessfully) {
  auto* actor_service = ActorKeyedService::Get(profile());
  actor_service->CreateTask(TestTaskSourceInfo(), NoEnterprisePolicyChecker());
  const TaskId id2 = actor_service->CreateTask(TestTaskSourceInfo(),
                                               NoEnterprisePolicyChecker());
  actor_service->GetTask(id2)->Pause(/*from_actor=*/true);

  // Find a single active task.
  std::vector<TaskId> single_found =
      actor_service->FindTaskIdsInActive([](const ActorTask& task) {
        return task.GetState() == ActorTask::State::kPausedByActor;
      });
  ASSERT_EQ(single_found.size(), 1u);
  EXPECT_EQ(single_found[0], id2);
}

// Test that adding a tab to a paused or stopped task has no effect.
TEST_F(ActorKeyedServiceTest, AddTabToPausedOrStoppedTask) {
  auto* actor_service = ActorKeyedService::Get(profile());
  TaskId id = actor_service->CreateTask(TestTaskSourceInfo(),
                                        NoEnterprisePolicyChecker());

  base::WeakPtr<ActorTask> task = actor_service->GetTask(id)->GetWeakPtr();
  ASSERT_TRUE(task);
  const tabs::TabHandle tab_handle(123);

  // Pause the task and try to add a tab.
  task->Pause(/*from_actor=*/true);
  EXPECT_TRUE(task->IsUnderUserControl());

  {
    base::RunLoop loop;
    task->AddTab(tab_handle,
                 /*stop_task_on_detach=*/true,
                 base::BindLambdaForTesting([&](mojom::ActionResultPtr result) {
                   EXPECT_EQ(result->code,
                             mojom::ActionResultCode::kTaskPaused);
                   loop.Quit();
                 }));
    loop.Run();
  }
  EXPECT_FALSE(task->IsActingOnTab(tab_handle));
  EXPECT_FALSE(task->HasTab(tab_handle));

  // Stop the task and ensure it is gone.
  actor_service->StopTask(id, ActorTask::StoppedReason::kTaskComplete);
  WaitForPostedTask();
  EXPECT_FALSE(task);
}

// Test tab association to a paused task.
TEST_F(ActorKeyedServiceTest, PausedTaskTabs) {
  auto* actor_service = ActorKeyedService::Get(profile());
  TaskId id = actor_service->CreateTask(TestTaskSourceInfo(),
                                        NoEnterprisePolicyChecker());

  base::WeakPtr<ActorTask> task = actor_service->GetTask(id)->GetWeakPtr();
  ASSERT_TRUE(task);
  const tabs::TabHandle tab_handle(123);

  {
    base::test::TestFuture<mojom::ActionResultPtr> future;
    task->AddTab(tab_handle, /*stop_task_on_detach=*/true,
                 future.GetCallback());
    ASSERT_TRUE(future.Wait());
  }

  // The tab should be both part of the task and actively acting on it when in a
  // created or acting state.

  EXPECT_TRUE(task->IsActingOnTab(tab_handle));
  EXPECT_TRUE(task->HasTab(tab_handle));

  task->SetState(ActorTask::State::kActing);

  EXPECT_TRUE(task->IsActingOnTab(tab_handle));
  EXPECT_TRUE(task->HasTab(tab_handle));

  task->SetState(ActorTask::State::kReflecting);

  EXPECT_TRUE(task->IsActingOnTab(tab_handle));
  EXPECT_TRUE(task->HasTab(tab_handle));

  // Pausing the task should keep the tab in the task but it should no longer be
  // considered acting.

  task->Pause(true);

  EXPECT_FALSE(task->IsActingOnTab(tab_handle));
  EXPECT_TRUE(task->HasTab(tab_handle));

  task->Resume();

  EXPECT_TRUE(task->IsActingOnTab(tab_handle));
  EXPECT_TRUE(task->HasTab(tab_handle));

  task->Pause(false);

  EXPECT_FALSE(task->IsActingOnTab(tab_handle));
  EXPECT_TRUE(task->HasTab(tab_handle));

  task->Resume();

  EXPECT_TRUE(task->IsActingOnTab(tab_handle));
  EXPECT_TRUE(task->HasTab(tab_handle));

  // Stop the task. This should (asynchronously) remove the tab from the task.
  actor_service->StopTask(id, ActorTask::StoppedReason::kTaskComplete);
  WaitForPostedTask();
  EXPECT_FALSE(task);
}

TEST_F(ActorKeyedServiceTest, SetsTaskSourceInfo) {
  auto* actor_service = ActorKeyedService::Get(profile());
  const TaskSourceInfo::SourceDefinedId kId1 = "task1id";
  const TaskSourceInfo::SourceDefinedId kId2 = "task2id";

  TaskId task1 = actor_service->CreateTask(
      TaskSourceInfo(TaskSourceInfo::Client::kTest, kId1),
      NoEnterprisePolicyChecker());
  TaskId task2 = actor_service->CreateTask(
      TaskSourceInfo(TaskSourceInfo::Client::kTest, kId2),
      NoEnterprisePolicyChecker());

  EXPECT_EQ(actor_service->GetTask(task1)->source_info().id, kId1);
  EXPECT_EQ(actor_service->GetTask(task2)->source_info().id, kId2);
}

// Tests that GetActiveTasks() can be called from a TaskStateChangedCallback
// without crashing, even when a task is completing.
TEST_F(ActorKeyedServiceTest, GetActiveTasksDuringStateChangeCallback) {
  auto* actor_service = ActorKeyedService::Get(profile());
  TaskId id = actor_service->CreateTask(TestTaskSourceInfo(),
                                        NoEnterprisePolicyChecker());

  bool callback_called = false;
  auto subscription = actor_service->AddTaskStateChangedCallback(
      base::BindLambdaForTesting([&](ActorTask& task) {
        if (ActorTask::IsCompletedState(task.GetState())) {
          // This should not crash. Repro for crash in http://b/493610427.
          actor_service->GetActiveTasks();
          callback_called = true;
        }
      }));

  actor_service->StopTask(id, ActorTask::StoppedReason::kTaskComplete);
  EXPECT_TRUE(callback_called);
}

TEST_F(ActorKeyedServiceTest, InitialTabAssociationOnCreate) {
  auto* actor_service = ActorKeyedService::Get(profile());
  const tabs::TabHandle tab_handle(123);
  auto options = webui::mojom::TaskOptions::New();
  options->actuation_tab_id = tab_handle.raw_value();

  TaskId id = actor_service->CreateTaskWithOptions(
      TestTaskSourceInfo(), NoEnterprisePolicyChecker(), std::move(options),
      /*delegate=*/nullptr);

  ActorTask* task = actor_service->GetTask(id);
  ASSERT_TRUE(task);
  EXPECT_TRUE(task->HasTab(tab_handle));
  EXPECT_TRUE(task->IsActingOnTab(tab_handle));
  EXPECT_EQ(task->GetTabs().size(), 1u);
  EXPECT_TRUE(task->GetTabs().contains(tab_handle));
}

}  // namespace

}  // namespace actor
