// Copyright 2026 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/glic/actor/glic_actor_functional_browsertest.h"
#include "chrome/browser/glic/actor/glic_actor_test_util.h"
#include "chrome/common/webui_url_constants.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/actor/public/mojom/actor_types.mojom.h"
#include "content/public/test/browser_test.h"

namespace glic::actor {
namespace {

using ::actor::ActorTask;
using ::actor::TaskId;
using ::base::test::TestFuture;
using ::base::test::ValueIs;
using ::optimization_guide::proto::Actions;
using ::optimization_guide::proto::ActionsResult;
using ::optimization_guide::proto::ClickAction;
using ::optimization_guide::proto::ScrollAction;
using ::optimization_guide::proto::TabObservation;

class GlicActorActionExecutionFunctionalBrowserTest
    : public GlicActorFunctionalBrowserTestBase {
 public:
  GlicActorActionExecutionFunctionalBrowserTest() = default;
  ~GlicActorActionExecutionFunctionalBrowserTest() override = default;
};

IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
                       PerformNavigateAction) {
  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
  EXPECT_NE(task_id, TaskId());

  TestFuture<ActorTask::State> task_completion_state;
  base::CallbackListSubscription subscription =
      CreateTaskCompletionSubscription(task_id, task_completion_state);

  // Construct the Actions proto.
  const GURL target_url =
      embedded_test_server()->GetURL("/actor/blank.html?target");
  Actions action = ::actor::MakeNavigate(active_tab()->GetHandle(),
                                         target_url.spec(), task_id);

  ASSERT_OK_AND_ASSIGN(ActionsResult result, PerformActions(action));
  EXPECT_THAT(result, HasResultCode(::actor::mojom::ActionResultCode::kOk));
  // At the moment, no actions in GLIC set a non-empty extra_information value.
  // Therefore, we just verify that the empty string is set.
  ASSERT_EQ(result.extra_information_size(), 1);
  EXPECT_EQ(result.extra_information(0), "");
  EXPECT_EQ(target_url, web_contents()->GetURL());

  StopActorTask(task_id, glic::mojom::ActorTaskStopReason::kTaskComplete);

  EXPECT_EQ(ActorTask::State::kFinished, task_completion_state.Get())
      << "Task " << task_id << " did not reach kFinished state.";
}

IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
                       PerformClickAction) {
  // Set up the initial page with a link to the target page.
  const GURL initial_url = embedded_test_server()->GetURL("/actor/link.html");
  const GURL target_url = embedded_test_server()->GetURL("/actor/blank.html");
  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));
  EXPECT_TRUE(content::ExecJs(web_contents(),
                              content::JsReplace("setLink($1);", target_url)));

  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
  EXPECT_NE(task_id, TaskId());

  TestFuture<ActorTask::State> task_completion_state;
  base::CallbackListSubscription subscription =
      CreateTaskCompletionSubscription(task_id, task_completion_state);

  // Click link to navigate to target page.
  std::optional<int> link_node_id =
      content::GetDOMNodeId(*web_contents()->GetPrimaryMainFrame(), "#link");
  Actions action = ::actor::MakeClick(*web_contents()->GetPrimaryMainFrame(),
                                      link_node_id.value(), ClickAction::LEFT,
                                      ClickAction::SINGLE, task_id);

  EXPECT_THAT(PerformActions(action),
              ValueIs(HasResultCode(::actor::mojom::ActionResultCode::kOk)));
  EXPECT_EQ(target_url, web_contents()->GetURL());

  StopActorTask(task_id, glic::mojom::ActorTaskStopReason::kTaskComplete);
  EXPECT_EQ(ActorTask::State::kFinished, task_completion_state.Get())
      << "Task " << task_id << " did not reach kFinished state.";
}

using ClickActionFactory = ClickAction (*)(tabs::TabHandle);

ClickAction MakeNullTabClickAction(tabs::TabHandle handle) {
  ClickAction click;
  click.mutable_target()->mutable_coordinate()->set_x(0);
  click.mutable_target()->mutable_coordinate()->set_y(0);
  click.set_click_type(ClickAction::LEFT);
  click.set_click_count(ClickAction::SINGLE);
  return click;
}

ClickAction MakeMissingTargetClickAction(tabs::TabHandle handle) {
  ClickAction click;
  click.set_tab_id(handle.raw_value());
  click.set_click_type(ClickAction::LEFT);
  click.set_click_count(ClickAction::SINGLE);
  return click;
}

ClickAction MakeMissingTypeClickAction(tabs::TabHandle handle) {
  ClickAction click;
  click.set_tab_id(handle.raw_value());
  click.mutable_target()->mutable_coordinate()->set_x(0);
  click.mutable_target()->mutable_coordinate()->set_y(0);
  click.set_click_count(ClickAction::SINGLE);
  return click;
}

ClickAction MakeMissingCountClickAction(tabs::TabHandle handle) {
  ClickAction click;
  click.set_tab_id(handle.raw_value());
  click.mutable_target()->mutable_coordinate()->set_x(0);
  click.mutable_target()->mutable_coordinate()->set_y(0);
  click.set_click_type(ClickAction::LEFT);
  return click;
}

using ScrollActionFactory = ScrollAction (*)(tabs::TabHandle);

ScrollAction MakeNullTabScrollAction(tabs::TabHandle handle) {
  ScrollAction scroll;
  scroll.set_direction(ScrollAction::DOWN);
  scroll.set_distance(100.0f);
  return scroll;
}

ScrollAction MakeMissingDirectionScrollAction(tabs::TabHandle handle) {
  ScrollAction scroll;
  scroll.set_tab_id(handle.raw_value());
  scroll.set_distance(100.0f);
  return scroll;
}

ScrollAction MakeMissingDistanceScrollAction(tabs::TabHandle handle) {
  ScrollAction scroll;
  scroll.set_tab_id(handle.raw_value());
  scroll.set_direction(ScrollAction::DOWN);
  return scroll;
}

class GlicActorClickActionExecutionErrorBrowserTest
    : public GlicActorFunctionalBrowserTestBase,
      public ::testing::WithParamInterface<
          std::pair<ClickActionFactory, ::actor::mojom::ActionResultCode>> {
 public:
  GlicActorClickActionExecutionErrorBrowserTest() = default;
};

IN_PROC_BROWSER_TEST_P(GlicActorClickActionExecutionErrorBrowserTest,
                       PerformClickActionErrors) {
  // Set up the initial page with a link to the target page.
  const GURL initial_url = embedded_test_server()->GetURL("/actor/link.html");
  const GURL target_url = embedded_test_server()->GetURL("/actor/blank.html");
  ASSERT_TRUE(content::NavigateToURL(web_contents(), initial_url));
  EXPECT_TRUE(content::ExecJs(web_contents(),
                              content::JsReplace("setLink($1);", target_url)));

  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
  EXPECT_NE(task_id, TaskId());

  TestFuture<ActorTask::State> task_completion_state;
  base::CallbackListSubscription subscription =
      CreateTaskCompletionSubscription(task_id, task_completion_state);

  auto [action_factory, expected_result] = GetParam();

  Actions action;
  *action.add_actions()->mutable_click() =
      action_factory(active_tab()->GetHandle());
  action.set_task_id(task_id.value());

  ASSERT_OK_AND_ASSIGN(ActionsResult result, PerformActions(action));
  EXPECT_THAT(result, HasResultCode(expected_result));
  // In case of errors, no extra_information is set.
  EXPECT_EQ(result.extra_information_size(), 0);

  StopActorTask(task_id, glic::mojom::ActorTaskStopReason::kTaskComplete);
  EXPECT_EQ(ActorTask::State::kFinished, task_completion_state.Get())
      << "Task " << task_id << " did not reach kFinished state.";
}

INSTANTIATE_TEST_SUITE_P(
    GlicActorClickActionExecutionErrorBrowserTestAll,
    GlicActorClickActionExecutionErrorBrowserTest,
    ::testing::Values(
        std::make_pair(MakeNullTabClickAction,
                       ::actor::mojom::ActionResultCode::kTabWentAway),
        std::make_pair(MakeMissingTargetClickAction,
                       ::actor::mojom::ActionResultCode::kClickMissingTarget),
        std::make_pair(MakeMissingTypeClickAction,
                       ::actor::mojom::ActionResultCode::kClickMissingType),
        std::make_pair(MakeMissingCountClickAction,
                       ::actor::mojom::ActionResultCode::kClickInvalidCount)));

class GlicActorScrollActionExecutionErrorBrowserTest
    : public GlicActorFunctionalBrowserTestBase,
      public ::testing::WithParamInterface<
          std::pair<ScrollActionFactory, ::actor::mojom::ActionResultCode>> {
 public:
  GlicActorScrollActionExecutionErrorBrowserTest() = default;
};

IN_PROC_BROWSER_TEST_P(GlicActorScrollActionExecutionErrorBrowserTest,
                       PerformScrollActionErrors) {
  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
  EXPECT_NE(task_id, TaskId());

  TestFuture<ActorTask::State> task_completion_state;
  base::CallbackListSubscription subscription =
      CreateTaskCompletionSubscription(task_id, task_completion_state);

  auto [action_factory, expected_result] = GetParam();

  Actions action;
  *action.add_actions()->mutable_scroll() =
      action_factory(active_tab()->GetHandle());
  action.set_task_id(task_id.value());

  EXPECT_THAT(PerformActions(action), ValueIs(HasResultCode(expected_result)));

  StopActorTask(task_id, glic::mojom::ActorTaskStopReason::kTaskComplete);
  EXPECT_EQ(ActorTask::State::kFinished, task_completion_state.Get())
      << "Task " << task_id << " did not reach kFinished state.";
}

INSTANTIATE_TEST_SUITE_P(
    GlicActorScrollActionExecutionErrorBrowserTestAll,
    GlicActorScrollActionExecutionErrorBrowserTest,
    ::testing::Values(
        std::make_pair(MakeNullTabScrollAction,
                       ::actor::mojom::ActionResultCode::kTabWentAway),
        std::make_pair(
            MakeMissingDirectionScrollAction,
            ::actor::mojom::ActionResultCode::kScrollMissingDirection),
        std::make_pair(
            MakeMissingDistanceScrollAction,
            ::actor::mojom::ActionResultCode::kScrollMissingDistance)));

IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
                       PerformConcurrentAsyncWaitActions) {
  // Manually create tasks via ActorKeyedService.
  TaskId task_id_1 = actor_keyed_service()->CreateTask(
      MockGlicTaskSourceInfo(), ::actor::NoEnterprisePolicyChecker());
  TaskId task_id_2 = actor_keyed_service()->CreateTask(
      MockGlicTaskSourceInfo(), ::actor::NoEnterprisePolicyChecker());

  // Create tabs for each task using CreateActorTab API to ensure a
  // TabObservation is included in its result.
  ASSERT_OK_AND_ASSIGN(
      tabs::TabHandle tab_1,
      CreateActorTab(task_id_1, /*open_in_background=*/false,
                     base::ToString(active_tab()->GetHandle().raw_value()),
                     base::ToString(browser()->GetSessionID().id())));
  ASSERT_OK_AND_ASSIGN(
      tabs::TabHandle tab_2,
      CreateActorTab(task_id_2, /*open_in_background=*/false,
                     base::ToString(active_tab()->GetHandle().raw_value()),
                     base::ToString(browser()->GetSessionID().id())));

  // Perform two WaitActions where the first resolves after the second
  Actions action_1 = ::actor::MakeWait(kShortWaitTime * 2, tab_1, task_id_1);
  Actions action_2 = ::actor::MakeWait(kShortWaitTime, tab_2, task_id_2);

  std::unique_ptr<AsyncActionWaiter> waiter_1 = PerformActionsAsync(action_1);
  std::unique_ptr<AsyncActionWaiter> waiter_2 = PerformActionsAsync(action_2);

  // We should still be able to wait for result_2 after result_1 despite
  // action_2 resolving first.
  ASSERT_OK_AND_ASSIGN(ActionsResult result_1, waiter_1->Wait());
  ASSERT_OK_AND_ASSIGN(ActionsResult result_2, waiter_2->Wait());

  // Verify a tab observation was included in the results.
  EXPECT_THAT(result_1, HasResultCode(::actor::mojom::ActionResultCode::kOk));
  EXPECT_THAT(result_1.tabs(), testing::SizeIs(1));
  EXPECT_THAT(result_1.tabs().at(0).result(),
              TabObservation::TAB_OBSERVATION_OK);

  EXPECT_THAT(result_2, HasResultCode(::actor::mojom::ActionResultCode::kOk));
  EXPECT_THAT(result_2.tabs(), testing::SizeIs(1));
  EXPECT_THAT(result_2.tabs().at(0).result(),
              TabObservation::TAB_OBSERVATION_OK);
}

IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
                       CloseTabWhileActing) {
  base::HistogramTester histogram_tester;

  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
  EXPECT_NE(task_id, TaskId());

  TestFuture<ActorTask::State> task_completion_state;
  base::CallbackListSubscription subscription =
      CreateTaskCompletionSubscription(task_id, task_completion_state);

  Actions wait_action =
      ::actor::MakeWait(kLongWaitTime, active_tab()->GetHandle(), task_id);
  std::unique_ptr<AsyncActionWaiter> action_waiter =
      PerformActionsAsync(wait_action);

  // Wait for the task to start acting before closing the tab.
  WaitForTaskState(task_id, ActorTask::State::kActing);

  // Add a new background tab to prevent the browser from closing.
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), GURL(url::kAboutBlankURL),
      WindowOpenDisposition::NEW_BACKGROUND_TAB,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_LOAD_STOP);

  // Close the active web contents.
  browser()->tab_strip_model()->CloseWebContents(web_contents(),
                                                 TabCloseTypes::CLOSE_NONE);

  // After an acting tab is closed, the task should be cancelled and the
  // corresponding action have a result code of kTaskWentAway.
  // NOTE: We cannot use `action_waiter->Wait()` to check the result code
  // because the test client is destroyed when all task tabs are closed.
  EXPECT_EQ(ActorTask::State::kCancelled, task_completion_state.Get());
  histogram_tester.ExpectUniqueSample(
      "Actor.ExecutionEngine.Action.ResultCode",
      ::actor::mojom::ActionResultCode::kTaskWentAway, 1);
}

IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
                       PerformActionsOnCrashedTabReloadsTab) {
  const GURL& initial_url = web_contents()->GetLastCommittedURL();
  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
  ASSERT_NE(task_id, TaskId());

  TestFuture<ActorTask::State> task_completion_state;
  base::CallbackListSubscription subscription =
      CreateTaskCompletionSubscription(task_id, task_completion_state);

  // Crash the tab.
  content::CrashTab(web_contents());

  // Perform a click action on the crashed tab.
  Actions action =
      ::actor::MakeClick(active_tab()->GetHandle(), gfx::Point(1, 1),
                         ClickAction::LEFT, ClickAction::SINGLE, task_id);

  content::TestNavigationManager reload_observer(web_contents(), initial_url);
  EXPECT_THAT(PerformActions(action),
              ValueIs(HasResultCode(
                  ::actor::mojom::ActionResultCode::kRendererCrashed)));
  EXPECT_TRUE(reload_observer.WaitForNavigationFinished());
  EXPECT_FALSE(web_contents()->IsCrashed());
}

IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
                       CancelActions) {
  // Makes sure we are on about:blank so the browser won't open a new tab to
  // navigate.
  ASSERT_TRUE(
      content::NavigateToURL(web_contents(), GURL(url::kAboutBlankURL)));

  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
  ASSERT_NE(task_id, TaskId());
  const GURL target_url = embedded_test_server()->GetURL("/title1.html");
  content::TestNavigationManager navigation_manager(web_contents(), target_url);

  Actions action = ::actor::MakeNavigate(active_tab()->GetHandle(),
                                         target_url.spec(), task_id);
  std::unique_ptr<AsyncActionWaiter> waiter = PerformActionsAsync(action);

  // WaitForRequestStart() also pauses the navigation.
  EXPECT_TRUE(navigation_manager.WaitForRequestStart());
  EXPECT_EQ(actor_keyed_service()->GetTask(task_id)->GetState(),
            ActorTask::State::kActing);
  EXPECT_THAT(CancelActions(task_id),
              base::test::ValueIs(glic::mojom::CancelActionsResult::kSuccess));
  EXPECT_FALSE(navigation_manager.was_committed());
  EXPECT_EQ(actor_keyed_service()->GetTask(task_id)->GetState(),
            ActorTask::State::kReflecting);
  auto result = waiter->Wait();
  EXPECT_TRUE(result.has_value());
  EXPECT_THAT(
      result.value(),
      HasResultCode(::actor::mojom::ActionResultCode::kActionsCancelled));
}

IN_PROC_BROWSER_TEST_F(GlicActorActionExecutionFunctionalBrowserTest,
                       CancelActionsNoActionsToCancel) {
  ASSERT_OK_AND_ASSIGN(TaskId task_id, CreateTask());
  EXPECT_NE(task_id, TaskId());
  EXPECT_EQ(actor_keyed_service()->GetTask(task_id)->GetState(),
            ActorTask::State::kCreated);
  EXPECT_THAT(CancelActions(task_id),
              base::test::ValueIs(glic::mojom::CancelActionsResult::kSuccess));
  EXPECT_EQ(actor_keyed_service()->GetTask(task_id)->GetState(),
            ActorTask::State::kCreated);
}

}  // namespace
}  // namespace glic::actor
