// Copyright 2018 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/autofill/captured_sites_test_utils.h"

#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <utility>
#include <variant>
#include <vector>

#include "base/base_switches.h"
#include "base/check_deref.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/json/json_reader.h"
#include "base/json/json_string_value_serializer.h"
#include "base/logging.h"
#include "base/notreached.h"
#include "base/path_service.h"
#include "base/process/launch.h"
#include "base/strings/strcat.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_split.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/task/thread_pool.h"
#include "base/test/bind.h"
#include "base/test/test_future.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "base/time/time_override.h"
#include "base/uuid.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/autofill/autofill_uitest_util.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/autofill/core/browser/autofill_type.h"
#include "components/autofill/core/browser/data_model/addresses/autofill_profile.h"
#include "components/autofill/core/browser/data_model/payments/credit_card.h"
#include "components/autofill/core/browser/field_types.h"
#include "components/autofill/core/browser/test_utils/autofill_test_utils.h"
#include "components/autofill/core/browser/test_utils/test_autofill_clock.h"
#include "components/autofill/core/common/autofill_clock.h"
#include "components/javascript_dialogs/app_modal_dialog_controller.h"
#include "components/javascript_dialogs/app_modal_dialog_view.h"
#include "components/permissions/permission_request_manager.h"
#include "components/variations/variations_switches.h"
#include "content/public/browser/browsing_data_remover.h"
#include "content/public/browser/navigation_handle.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/browser/web_contents_observer.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/browsing_data_remover_test_util.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/test_utils.h"
#include "net/base/address_list.h"
#include "net/base/network_handle.h"
#include "net/socket/tcp_client_socket.h"
#include "third_party/zlib/google/compression_utils.h"
#include "ui/events/keycodes/dom/dom_key.h"
#include "ui/events/keycodes/keyboard_code_conversion.h"
#include "ui/events/keycodes/keyboard_codes.h"
#include "url/gurl.h"
#include "url/scheme_host_port.h"

using base::JSONParserOptions;
using base::JSONReader;

namespace {

// The command-line flag to specify the command file flag.
const char kCommandFileFlag[] = "command_file";

// The command line flag to turn on verbose WPR logging.
const char kWebPageReplayVerboseFlag[] = "wpr_verbose";

// The maximum amount of time to wait for Chrome to finish autofilling a form.
const base::TimeDelta kAutofillActionWaitForVisualUpdateTimeout =
    base::Seconds(3);

// The number of tries the TestRecipeReplayer should perform when executing an
// Chrome Autofill action.
// Chrome Autofill can be flaky on some real-world pages. The Captured Site
// Automation Framework will retry an autofill action a couple times before
// concluding that Chrome Autofill does not work.
const int kAutofillActionNumRetries = 5;

// The public key hash for the certificate Web Page Replay (WPR) uses to serve
// HTTPS content.
// The Captured Sites Test Framework relies on WPR to serve captured site
// traffic. If a machine does not have the WPR certificate installed, Chrome
// will detect a server certificate validation failure when WPR serves Chrome
// HTTPS content. In response Chrome will block the WPR HTTPS content.
// The test framework avoids this problem by launching Chrome with the
// ignore-certificate-errors-spki-list flag set to the WPR certificate's
// public key hash. Doing so tells Chrome to ignore server certificate
// validation errors from WPR.
const char kWebPageReplayCertSPKI[] =
    "PoNnQAwghMiLUPg1YNFtvTfGreNT8r9oeLEyzgNCJWc=";

const char kTimeClockOverrideNotSetMessage[] =
    "No TimeClock override set from wpr archive: ";

// A helper class to override the current time.
struct TimeOverrideHelper {
  static base::Time TimeNow() { return current_time; }

  // Used as the current time in captured sites tests.
  static base::Time current_time;
};

base::Time TimeOverrideHelper::current_time;

// Check and return that the caller wants verbose WPR output (off by default).
bool IsVerboseWprLoggingEnabled() {
  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
  return command_line && command_line->HasSwitch(kWebPageReplayVerboseFlag);
}

void PrintDebugInstructions(const base::FilePath& command_file_path) {
  const char msg[] = R"(

*** INTERACTIVE DEBUGGING MODE ***

To proceed, you should create a named pipe:
  $ mkfifo %1$s
and then write commands into it:
  $ echo run               >%1$s  # unpauses execution
  $ echo run until failure >%1$s  # executes until failure
  $ echo run until foo     >%1$s  # executes until the next action is of type foo (e.g., "autofill")
  $ echo next 2            >%1$s  # executes the next 2 actions
  $ echo next -1           >%1$s  # executes until the last action
  $ echo skip -3           >%1$s  # jumps back 3 actions
  $ echo skip 4            >%1$s  # skips the next 4 actions
  $ echo where             >%1$s  # prints the current position
  $ echo show -1           >%1$s  # prints last 1 actions
  $ echo show 1            >%1$s  # prints next 1 actions
  $ echo help              >%1$s  # prints this text
)";
  VLOG(1) << base::StringPrintf(msg, command_file_path.AsUTF8Unsafe().c_str());
}

std::optional<autofill::FieldType> StringToFieldType(std::string_view str) {
  static auto map = [] {
    std::map<std::string_view, autofill::FieldType> map;
    for (autofill::FieldType field_type : autofill::FieldTypeSet::all()) {
      map[autofill::FieldTypeToStringView(field_type)] = field_type;
    }
    for (autofill::HtmlFieldType html_field_type :
         autofill::HtmlFieldTypeSet::all()) {
      map[autofill::FieldTypeToStringView(html_field_type)] =
          autofill::HtmlFieldTypeToBestCorrespondingFieldType(html_field_type);
    }
    return map;
  }();
  auto it = map.find(str);
  if (it == map.end()) {
    return std::nullopt;
  }
  return it->second;
}

struct ExecutionCommand {
  enum class Type {
    // Resumes execution and stops when the next action's index is equal to or
    // greater than `param_int`.
    kRunWithAbsoluteLimit,
    // Resumes execution and stops after `param_int` actions.
    kRunWithRelativeLimit,
    // Resumes execution and stops when the next action's type is
    // `param_string`.
    kRunUntilAction,
    // Resumes execution and stops when a failure happens.
    kRunUntilFailure,
    // Jumps `param_int` actions forward or backward.
    kSkipAction,
    // Prints the `param_int` previous (if < 0) or upcoming (if > 0) actions.
    kShowAction,
    // Prints the current execution position.
    kWhereAmI,
  };

  const Type type = Type::kRunWithAbsoluteLimit;
  const int param_int = std::numeric_limits<int>::max();
  const std::string param_string;
};

// Blockingly reads the content of `command_file_path`, parses it into
// ExecutionCommands, and returns the result.
std::vector<ExecutionCommand> ReadExecutionCommands(
    const base::FilePath& command_file_path) {
  std::vector<ExecutionCommand> commands;
  std::string command_lines;
  if (base::ReadFileToString(command_file_path, &command_lines)) {
    for (const auto command :
         base::SplitStringPiece(command_lines, "\n", base::TRIM_WHITESPACE,
                                base::SPLIT_WANT_NONEMPTY)) {
      auto GetParamOr = [command](int default_value) {
        size_t space = command.find(' ');
        if (space == std::string_view::npos) {
          return default_value;
        }
        int value;
        if (!base::StringToInt(command.substr(space + 1), &value))
          return default_value;
        return value;
      };

      if (command.starts_with("run until failure")) {
        commands.emplace_back(ExecutionCommand::Type::kRunUntilFailure);
      } else if (command.starts_with("run until ")) {
        static constexpr size_t kOffset =
            std::string_view("run until ").length();
        const std::string_view param =
            base::TrimWhitespaceASCII(command.substr(kOffset), base::TRIM_ALL);
        commands.emplace_back(ExecutionCommand::Type::kRunUntilAction, 0,
                              std::string(param));
      } else if (command.starts_with("run")) {
        commands.emplace_back(ExecutionCommand::Type::kRunWithAbsoluteLimit,
                              std::numeric_limits<int>::max());
      } else if (command.starts_with("next")) {
        commands.emplace_back(ExecutionCommand::Type::kRunWithRelativeLimit,
                              GetParamOr(1));
      } else if (command.starts_with("skip")) {
        commands.emplace_back(ExecutionCommand::Type::kSkipAction,
                              GetParamOr(1));
      } else if (command.starts_with("show")) {
        commands.emplace_back(ExecutionCommand::Type::kShowAction,
                              GetParamOr(1));
      } else if (command.starts_with("where")) {
        commands.emplace_back(ExecutionCommand::Type::kWhereAmI);
      } else if (command.starts_with("help")) {
        PrintDebugInstructions(command_file_path);
      }
    }
  }
  return commands;
}

struct ExecutionState {
  // The position of the next action to be executed.
  int index = 0;
  // The current bound on the execution: execution runs only if `index < limit`.
  int limit = std::numeric_limits<int>::max();
  // The number of actions to be executed.
  int length = 0;
  // Whether to stop at a step if a failure is detected.
  bool pause_on_failure = false;
};

// Blockingly reads the commands from `command_file_path` and executes them.
// Execution primarily means manipulation of the `execution_state`, particularly
// `execution_state.limit`.
ExecutionState ProcessCommands(ExecutionState execution_state,
                               const base::ListValue& action_list,
                               const base::FilePath& command_file_path) {
  while (execution_state.limit <= execution_state.index) {
    for (ExecutionCommand command : ReadExecutionCommands(command_file_path)) {
      switch (command.type) {
        case ExecutionCommand::Type::kRunWithAbsoluteLimit: {
          execution_state.limit = command.param_int;
          break;
        }
        case ExecutionCommand::Type::kRunWithRelativeLimit: {
          if (command.param_int >= 0) {
            execution_state.limit += command.param_int;
          } else {
            execution_state.limit = execution_state.length + command.param_int;
          }
          break;
        }
        case ExecutionCommand::Type::kRunUntilAction: {
          int offset_of_action = execution_state.index;
          while (offset_of_action < execution_state.length) {
            const base::DictValue* dict =
                action_list[offset_of_action].GetIfDict();
            if (!dict) {
              continue;
            }
            const std::string* type = dict->FindString("type");
            if (!type) {
              continue;
            }
            if (*type == command.param_string) {
              break;
            }
            ++offset_of_action;
          }
          execution_state.limit += offset_of_action;
          break;
        }
        case ExecutionCommand::Type::kRunUntilFailure: {
          VLOG(1) << "Will stop when a failure is found.";
          execution_state.limit = command.param_int;
          execution_state.pause_on_failure = true;
          break;
        }
        case ExecutionCommand::Type::kSkipAction: {
          execution_state.index += command.param_int;
          execution_state.index = std::min(std::max(execution_state.index, 0),
                                           execution_state.length - 1);
          break;
        }
        case ExecutionCommand::Type::kShowAction: {
          int min_index =
              execution_state.index + std::min(command.param_int, 0);
          int max_index =
              execution_state.index + std::max(command.param_int, 0);
          min_index = std::max(min_index, 0);
          max_index = std::min(max_index, execution_state.length);
          for (int i = min_index; i < max_index; ++i) {
            VLOG(1) << "Action " << (i - execution_state.index) << ": "
                    << action_list[i].DebugString();
          }
          break;
        }
        case ExecutionCommand::Type::kWhereAmI: {
          VLOG(1) << "Next action is at position " << execution_state.index
                  << ", limit (excl) is at " << execution_state.limit
                  << ", last (excl) is at " << execution_state.length;
          break;
        }
      }
    }
  }
  return execution_state;
}

struct AllowNull {
  inline constexpr AllowNull() = default;
};

std::optional<std::string> FindPopulateString(
    const base::DictValue& container,
    std::string_view key_name,
    std::variant<std::string_view, AllowNull> key_descriptor) {
  const std::string* value = container.FindString(key_name);
  if (!value) {
    if (std::holds_alternative<std::string_view>(key_descriptor)) {
      ADD_FAILURE() << "Failed to extract '"
                    << std::get<std::string_view>(key_descriptor)
                    << "' string from container!";
    }
    return std::nullopt;
  }

  return *value;
}

std::optional<std::vector<std::string>> FindPopulateStringVector(
    const base::DictValue& container,
    std::string_view key_name,
    std::variant<std::string_view, AllowNull> key_descriptor) {
  const base::ListValue* list = container.FindList(key_name);
  if (!list) {
    if (std::holds_alternative<std::string_view>(key_descriptor)) {
      ADD_FAILURE() << "Failed to extract '"
                    << std::get<std::string_view>(key_descriptor)
                    << "' strings from container!";
    }
    return std::nullopt;
  }

  std::vector<std::string> strings;
  for (const base::Value& item : *list) {
    if (!item.is_string()) {
      if (std::holds_alternative<std::string_view>(key_descriptor)) {
        ADD_FAILURE() << "Failed to extract element of '"
                      << std::get<std::string_view>(key_descriptor)
                      << "' vector from container!";
      }
      return std::nullopt;
    }
    strings.push_back(item.GetString());
  }
  return strings;
}

}  // namespace

namespace captured_sites_test_utils {

CapturedSiteParams::CapturedSiteParams() = default;
CapturedSiteParams::~CapturedSiteParams() = default;
CapturedSiteParams::CapturedSiteParams(const CapturedSiteParams& other) =
    default;

std::ostream& operator<<(std::ostream& os, const CapturedSiteParams& param) {
  if (param.scenario_dir.empty())
    return os << "Site: " << param.site_name;
  return os << "Scenario: " << param.scenario_dir
            << ", Site: " << param.site_name;
}

// Iterate through Autofill's Web Page Replay capture file directory to look
// for captures sites and automation recipe files. Return a list of sites for
// which recipe-based testing is available.
std::vector<CapturedSiteParams> GetCapturedSites(
    const base::FilePath& replay_files_dir_path) {
  std::vector<CapturedSiteParams> sites;
  base::FilePath config_file_path =
      replay_files_dir_path.AppendASCII("testcases.json");

  std::string json_text;
  if (!base::ReadFileToString(config_file_path, &json_text)) {
    LOG(WARNING) << "Could not read json file: " << config_file_path;
    LOG(WARNING)
        << "Did you forget to set checkout_chromium_autofill_test_dependencies "
           "to True in .gclient and gclient sync?";
    return sites;
  }
  // Parse json text content to json value node.
  auto value_with_error = JSONReader::ReadAndReturnValueWithError(
      json_text, JSONParserOptions::JSON_PARSE_RFC);
  if (!value_with_error.has_value()) {
    LOG(WARNING) << "Could not load test config from json file: "
                 << "`testcases.json` because: "
                 << value_with_error.error().message;
    return sites;
  }
  base::DictValue root_node = std::move(*value_with_error).TakeDict();
  const base::ListValue* list_node = root_node.FindList("tests");
  if (!list_node) {
    LOG(WARNING) << "No tests found in `testcases.json` config";
    return sites;
  }

  bool also_run_disabled = GTEST_FLAG_GET(also_run_disabled_tests);

  for (auto& item_val : *list_node) {
    if (!item_val.is_dict()) {
      continue;
    }
    const base::DictValue& item = item_val.GetDict();
    CapturedSiteParams param;
    param.site_name = CHECK_DEREF(item.FindString("site_name"));

    if (const std::string* scenario_dir = item.FindString("scenario_dir")) {
      param.scenario_dir = *scenario_dir;
    }
    param.is_disabled = item.FindBool("disabled").value_or(false);

    const std::optional<int> bug_number = item.FindInt("bug_number");
    if (bug_number) {
      param.bug_number = bug_number.value();
    }
    if (param.is_disabled && !also_run_disabled)
      continue;

    const std::string* expectation_string = item.FindString("expectation");
    if (expectation_string && *expectation_string == "FAIL") {
      param.expectation = kFail;
    } else {
      param.expectation = kPass;
    }
    // Check that a pair of .test and .wpr files exist - otherwise skip
    base::FilePath file_name = replay_files_dir_path;
    base::FilePath refresh_file_path =
        replay_files_dir_path.AppendASCII("refresh");
    if (!param.scenario_dir.empty()) {
      file_name = file_name.AppendASCII(param.scenario_dir);
      refresh_file_path = refresh_file_path.AppendASCII(param.scenario_dir);
    }
    file_name = file_name.AppendASCII(param.site_name);
    refresh_file_path =
        refresh_file_path.AppendASCII(param.site_name).AddExtensionASCII("wpr");

    base::FilePath capture_file_path = file_name.AddExtensionASCII("wpr");
    if (!base::PathExists(capture_file_path)) {
      LOG(WARNING) << "Test `" << param.site_name
                   << "` had no matching .wpr file";
      continue;
    }
    base::FilePath recipe_file_path = file_name.AddExtensionASCII("test");
    if (!base::PathExists(recipe_file_path)) {
      LOG(WARNING) << "Test `" << param.site_name
                   << "` had no matching .test file";
      continue;
    }
    param.capture_file_path = capture_file_path;
    param.recipe_file_path = recipe_file_path;
    param.refresh_file_path = refresh_file_path;
    sites.push_back(param);
  }
  return sites;
}

std::optional<base::DictValue> ReadRecipeFile(
    const base::FilePath& recipe_file_path) {
  // Read the text of the recipe file.
  base::ScopedAllowBlockingForTesting for_testing;
  std::string json_text;
  if (!base::ReadFileToString(recipe_file_path, &json_text)) {
    ADD_FAILURE() << "Failed to read recipe file '" << recipe_file_path << "'!";
    return std::nullopt;
  }

  // Convert the file text into a json object.
  std::optional<base::Value> parsed_json =
      base::JSONReader::Read(json_text, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!parsed_json) {
    ADD_FAILURE() << "Failed to deserialize json text!";
    return std::nullopt;
  }
  DCHECK(parsed_json->is_dict());
  return std::move(*parsed_json).TakeDict();
}

std::string FilePathToUTF8(const base::FilePath::StringType& str) {
#if BUILDFLAG(IS_WIN)
  return base::WideToUTF8(str);
#else
  return str;
#endif
}

std::optional<base::FilePath> GetCommandFilePath() {
  base::CommandLine* command_line = base::CommandLine::ForCurrentProcess();
  if (command_line && command_line->HasSwitch(kCommandFileFlag)) {
    return std::make_optional(
        command_line->GetSwitchValuePath(kCommandFileFlag));
  }
  return std::nullopt;
}

void PrintInstructions(const char* test_file_name) {
  const char msg[] = R"(

*** README ***

A captured-site test replays an action recipe (*.test)
against recorded web traffic (*.wpr).
These files need to be downloaded separately.

Recommended flags are:
  --test-launcher-timeout=1000000
  --ui-test-action-max-timeout=1000000
  --enable-pixel-output-in-tests
  --vmodule=captured_sites_test_utils=1,%s=1:

For interactive debugging, specify a command file:
  --%s=/path/to/file
Commands to step through the test can be written into that file.
Further instructions will be printed then.
)";
  VLOG(1) << base::StringPrintf(msg, test_file_name, kCommandFileFlag);
}

content::RenderFrameHost* WaitForFrameMatchingName(
    content::WebContents& web_contents,
    const std::string& name,
    base::TimeDelta timeout) {
  return WaitForFrame(web_contents,
                      base::BindRepeating(&content::FrameMatchesName, name),
                      timeout);
}

content::RenderFrameHost* WaitForFrameMatchingOrigin(
    content::WebContents& web_contents,
    const url::SchemeHostPort& origin,
    base::TimeDelta timeout) {
  return WaitForFrame(
      web_contents,
      base::BindRepeating(
          [](const url::SchemeHostPort& origin,
             content::RenderFrameHost* frame) {
            return url::SchemeHostPort(frame->GetLastCommittedURL()) == origin;
          },
          origin),
      timeout);
}

content::RenderFrameHost* WaitForFrameMatchingUrl(
    content::WebContents& web_contents,
    const GURL& url,
    base::TimeDelta timeout) {
  return WaitForFrame(web_contents,
                      base::BindRepeating(&content::FrameHasSourceUrl, url),
                      timeout);
}

content::RenderFrameHost* WaitForFrame(
    content::WebContents& web_contents,
    base::RepeatingCallback<bool(content::RenderFrameHost*)> predicate,
    base::TimeDelta timeout) {
  class IframeWaiter : public content::WebContentsObserver {
   public:
    IframeWaiter(
        content::WebContents* web_contents,
        base::RepeatingCallback<bool(content::RenderFrameHost*)> predicate)
        : content::WebContentsObserver(web_contents),
          predicate_(std::move(predicate)) {}
    IframeWaiter(const IframeWaiter&) = delete;
    IframeWaiter& operator=(const IframeWaiter&) = delete;
    ~IframeWaiter() override = default;

    content::GlobalRenderFrameHostId Get() { return future_.Get(); }

    // content::WebContentsObserver
    void RenderFrameCreated(
        content::RenderFrameHost* render_frame_host) override {
      if (!future_.IsReady() && predicate_.Run(render_frame_host)) {
        future_.SetValue(render_frame_host->GetGlobalId());
      }
    }

    void DidFinishLoad(content::RenderFrameHost* render_frame_host,
                       const GURL& validated_url) override {
      if (!future_.IsReady() && predicate_.Run(render_frame_host)) {
        future_.SetValue(render_frame_host->GetGlobalId());
      }
    }

    void FrameNameChanged(content::RenderFrameHost* render_frame_host,
                          const std::string& name) override {
      if (!future_.IsReady() && predicate_.Run(render_frame_host)) {
        future_.SetValue(render_frame_host->GetGlobalId());
      }
    }

   private:
    // When we detect that a frame satisfies the `predicate_`, we store its ID
    // in `future_` and return it.
    base::RepeatingCallback<bool(content::RenderFrameHost*)> predicate_;
    base::test::TestFuture<content::GlobalRenderFrameHostId> future_;
  };

  if (content::RenderFrameHost* frame = FrameMatchingPredicateOrNullptr(
          web_contents.GetPrimaryPage(), predicate)) {
    return frame;
  }
  IframeWaiter waiter(&web_contents, std::move(predicate));
  base::test::ScopedRunLoopTimeout scoped_timeout(
      FROM_HERE, timeout, base::BindRepeating([]() -> std::string {
        return "IframeWaiter timed out waiting for iframe.";
      }));
  return content::RenderFrameHost::FromID(waiter.Get());
}

// WebPageReplayServerWrapper -------------------------------------------------
WebPageReplayServerWrapper::WebPageReplayServerWrapper(
    const bool start_as_replay,
    int host_http_port,
    int host_https_port,
    std::vector<std::string> extra_args)
    : host_http_port_(host_http_port),
      host_https_port_(host_https_port),
      start_as_replay_(start_as_replay),
      extra_args_(std::move(extra_args)) {}

WebPageReplayServerWrapper::~WebPageReplayServerWrapper() = default;

bool WebPageReplayServerWrapper::Start(
    const base::FilePath& capture_file_path) {
  std::vector<std::string> args;
  base::FilePath src_dir;
  if (!base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &src_dir)) {
    ADD_FAILURE() << "Failed to extract the Chromium source directory!";
    return false;
  }

  args.push_back(base::StringPrintf("--http_port=%d", host_http_port_));
  args.push_back(base::StringPrintf("--https_port=%d", host_https_port_));
  if (start_as_replay_) {
    args.push_back("--serve_response_in_chronological_sequence");
    // Start WPR in quiet mode, removing the extra verbose ServeHTTP
    // interactions that are for the the overwhelming majority unhelpful, but
    // for extra debugging of a test case, include 'wpr_verbose' in command.
    if (!IsVerboseWprLoggingEnabled())
      args.push_back("--quiet_mode");
  }
  args.push_back(base::StringPrintf(
      "--inject_scripts=%s",
      FilePathToUTF8(src_dir.AppendASCII("chrome")
                         .AppendASCII("test")
                         .AppendASCII("data")
                         .AppendASCII("web_page_replay_go_helper_scripts")
                         .AppendASCII("automation_helper.js")
                         .value())
          .c_str()));
  args.push_back("--no_archive_certificates");

  // Specify the capture file.
  args.push_back(base::StringPrintf(
      "%s", FilePathToUTF8(capture_file_path.value()).c_str()));
  if (!RunWebPageReplayCmd(args))
    return false;

  base::RunLoop wpr_launch_waiter;
  // This socket is created and destroyed on the IO thread, but it's simplest to
  // declare it here, since its deleted by a callback that may be invoked
  // asynchronously by its connect method, or calls its Connect method,
  // depending on whether the connection attempt completes synchronously. Having
  // two deletion paths makes BindOnce() not work well with it.
  std::unique_ptr<net::TCPClientSocket> client_socket;

  size_t connect_attempts = 0;
  base::RepeatingCallback<void(int)> on_connect_complete;
  auto bind_new_socket = base::BindLambdaForTesting([&]() {
    net::AddressList addr(
        net::IPEndPoint(net::IPAddress(127, 0, 0, 1), host_http_port_));
    ++connect_attempts;
    client_socket = std::make_unique<net::TCPClientSocket>(
        addr, nullptr, nullptr, nullptr, net::NetLogSource(),
        // No need to use a target network here. This is an external tool not
        // used in production.
        net::handles::kInvalidNetworkHandle);
    int connect_result = client_socket->Connect(on_connect_complete);
    // On ERR_IO_PENDING, `on_connect_complete` will be invoked
    // asynchronously, so need to let the message loop spin until that
    // happens.
    if (connect_result == net::ERR_IO_PENDING) {
      return;
    }
    // Otherwise, run `on_connect_complete` immediately.
    on_connect_complete.Run(connect_result);
  });

  // Called on the IO thread once connection has completed. Destroys the
  // `client_socket`, which may or may not be the object invoking the callback.
  on_connect_complete = base::BindLambdaForTesting([&](int connect_result) {
    client_socket.reset();
    if (connect_result == net::OK || connect_attempts > 30) {
      wpr_launch_waiter.Quit();
    } else {
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE, bind_new_socket, base::Seconds(1));
    }
  });

  content::GetIOThreadTaskRunner({})->PostTask(FROM_HERE, bind_new_socket);
  wpr_launch_waiter.Run();

  if (!web_page_replay_server_.IsValid()) {
    ADD_FAILURE() << "Failed to start the WPR replay server!";
    return false;
  }

  return true;
}

bool WebPageReplayServerWrapper::Stop() {
  if (web_page_replay_server_.IsValid()) {
    bool did_terminate = false;
    if (!start_as_replay_) {
#if BUILDFLAG(IS_POSIX)
      bool error_when_terminating_record_mode = false;
      // For Replay sessions, this function can terminate the WPR process
      // immediately as it doesn't need to write the archive file. In Record
      // sessions, this function will try to send a SIGINT and use
      // `WaitForExit()` to close and write the WPR archive file gracefully. If
      // this fails, the function will terminate via Process::Terminate which
      // will send SIGTERM and then SIGKILL.
      if (kill(web_page_replay_server_.Handle(), SIGINT) != 0) {
        ADD_FAILURE() << "Failed to send SIGINT to a recording WPR server!";
        error_when_terminating_record_mode = true;
      } else if (!web_page_replay_server_.WaitForExit(nullptr)) {
        ADD_FAILURE() << "Failed to wait for a recording WPR server to exit!";
        error_when_terminating_record_mode = true;
      }
      did_terminate = !error_when_terminating_record_mode;
#else
      ADD_FAILURE()
          << "Clean termination of recording WPR server is only supported on "
             "OS_POSIX. New archive may not be saved properly.";
#endif
    }
    if (start_as_replay_ || !did_terminate) {
      if (!web_page_replay_server_.Terminate(0, true)) {
        ADD_FAILURE() << "Failed to terminate the WPR replay server!";
        return false;
      }
    }
  }

  // The test server hasn't started, no op.
  return true;
}

bool WebPageReplayServerWrapper::RunWebPageReplayCmdAndWaitForExit(
    const std::vector<std::string>& args,
    const base::TimeDelta& timeout) {
  int exit_code;

  if (RunWebPageReplayCmd(args) && web_page_replay_server_.IsValid() &&
      web_page_replay_server_.WaitForExitWithTimeout(timeout, &exit_code) &&
      exit_code == 0) {
    return true;
  }

  ADD_FAILURE() << "Failed to run WPR command: '" << cmd_name() << "'!";
  return false;
}

bool WebPageReplayServerWrapper::RunWebPageReplayCmd(
    const std::vector<std::string>& args) {
  // Allow the function to block. Otherwise the subsequent call to
  // base::PathExists will fail. base::PathExists must be called from
  // a scope that allows blocking.
  base::ScopedAllowBlockingForTesting allow_blocking;

  base::LaunchOptions options = base::LaunchOptionsForTest();
  base::FilePath exe_dir;
  if (!base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &exe_dir)) {
    ADD_FAILURE() << "Failed to extract the Chromium source directory!";
    return false;
  }

  base::FilePath web_page_replay_binary_dir = exe_dir.AppendASCII("third_party")
                                                  .AppendASCII("webpagereplay")
                                                  .AppendASCII("scripts");
  options.current_directory = web_page_replay_binary_dir;
  base::FilePath wpr_executable_binary =
      base::FilePath(FILE_PATH_LITERAL("run_wpr.py"));
  base::CommandLine full_command(base::FilePath(FILE_PATH_LITERAL(
#if BUILDFLAG(IS_WIN)
      "vpython3.bat"
#else
      "vpython3"
#endif
      )));
  full_command.AppendArgPath(
      web_page_replay_binary_dir.Append(wpr_executable_binary));
  full_command.AppendArg(cmd_name());

  // Ask web page replay to use the custom certificate and key files used to
  // make the web page captures.
  // The capture files used in these browser tests are also used on iOS to
  // test autofill.
  // The custom cert and key files are different from those of the official
  // WPR releases. The custom files are made to work on iOS.
  base::FilePath src_dir;
  if (!base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &src_dir)) {
    ADD_FAILURE() << "Failed to extract the Chromium source directory!";
    return false;
  }

  base::FilePath web_page_replay_support_file_dir =
      src_dir.AppendASCII("components")
          .AppendASCII("test")
          .AppendASCII("data")
          .AppendASCII("autofill")
          .AppendASCII("web_page_replay_support_files");
  full_command.AppendArg(base::StringPrintf(
      "--https_cert_file=%s,%s",
      FilePathToUTF8(
          web_page_replay_support_file_dir.AppendASCII("wpr_cert.pem").value())
          .c_str(),
      FilePathToUTF8(
          web_page_replay_support_file_dir.AppendASCII("ecdsa_cert.pem")
              .value())
          .c_str()));
  full_command.AppendArg(base::StringPrintf(
      "--https_key_file=%s,%s",
      FilePathToUTF8(
          web_page_replay_support_file_dir.AppendASCII("wpr_key.pem").value())
          .c_str(),
      FilePathToUTF8(
          web_page_replay_support_file_dir.AppendASCII("ecdsa_key.pem").value())
          .c_str()));

  for (const auto& arg : extra_args_) {
    full_command.AppendArg(arg);
  }

  for (const auto& arg : args)
    full_command.AppendArg(arg);

  VLOG(1) << full_command.GetArgumentsString();

  web_page_replay_server_ = base::LaunchProcess(full_command, options);
  return true;
}

// ProfileDataController ------------------------------------------------------
ProfileDataController::ProfileDataController()
    : profile_(autofill::test::GetIncompleteProfile2()),
      card_(autofill::CreditCard(
          base::Uuid::GenerateRandomV4().AsLowercaseString())) {
  // Initialize the credit card with default values, in case the test recipe
  // file does not contain pre-saved credit card info.
  autofill::test::SetCreditCardInfo(&card_, "Buddy Holly", "5187654321098765",
                                    "10", "2998", "1");
}

ProfileDataController::~ProfileDataController() = default;

bool ProfileDataController::AddAutofillProfileInfo(
    const std::string& field_type,
    const std::string& field_value) {
  std::optional<autofill::FieldType> type = StringToFieldType(field_type);
  if (!type.has_value()) {
    ADD_FAILURE() << "Unable to recognize autofill field type '" << field_type
                  << "'!";
    return false;
  }

  // PhoneNumber only allows PHONE_HOME_WHOLE_NUMBER to be set directly, so if
  // any other phone type is set, convert it to PHONE_HOME_WHOLE_NUMBER.
  if (GroupTypeOfFieldType(type.value()) == autofill::FieldTypeGroup::kPhone &&
      type.value() != autofill::FieldType::PHONE_HOME_WHOLE_NUMBER) {
    type = autofill::FieldType::PHONE_HOME_WHOLE_NUMBER;
  }

  if (base::StartsWith(field_type, "HTML_TYPE_CREDIT_CARD_",
                       base::CompareCase::INSENSITIVE_ASCII) ||
      base::StartsWith(field_type, "CREDIT_CARD_",
                       base::CompareCase::INSENSITIVE_ASCII)) {
    if (type == autofill::CREDIT_CARD_VERIFICATION_CODE) {
      cvc_ = base::UTF8ToUTF16(field_value);
    }
    card_.SetRawInfo(type.value(), base::UTF8ToUTF16(field_value));
  } else {
    profile_.SetRawInfo(type.value(), base::UTF8ToUTF16(field_value));
  }

  return true;
}

// TestRecipeReplayer ---------------------------------------------------------
TestRecipeReplayer::TestRecipeReplayer(
    Browser* browser,
    TestRecipeReplayChromeFeatureActionExecutor* feature_action_executor)
    : browser_(browser), feature_action_executor_(feature_action_executor) {
  CleanupSiteData();
  // Bypass permission dialogs.
  permissions::PermissionRequestManager::FromWebContents(GetWebContents())
      ->set_auto_response_for_test(
          permissions::PermissionRequestManager::ACCEPT_ALL);
}

TestRecipeReplayer::~TestRecipeReplayer() {
  // If there are still cookies at the time the browser test shuts down,
  // Chrome's SQL lite persistent cookie store will crash.
  CleanupSiteData();
  EXPECT_TRUE(web_page_replay_server_wrapper()->Stop())
      << "Cannot stop the local Web Page Replay server.";
}

bool TestRecipeReplayer::ReplayTest(
    const base::FilePath& capture_file_path,
    const base::FilePath& recipe_file_path,
    const std::optional<base::FilePath>& command_file_path) {
  logging::SetMinLogLevel(logging::LOGGING_WARNING);
  if (!web_page_replay_server_wrapper()->Start(capture_file_path))
    return false;
  if (OverrideTimeClock(capture_file_path)) {
    VLOG(1) << "OverrideTimeClock was set to:" << base::Time::Now();
  }
  return ReplayRecordedActions(recipe_file_path, command_file_path);
}

const std::vector<testing::AssertionResult>
TestRecipeReplayer::GetValidationFailures() const {
  return validation_failures_;
}

// Extracts the time of the wpr recording from the wpr archive file and
// overrides the base::Time::Now() to match that time.
bool TestRecipeReplayer::OverrideTimeClock(
    const base::FilePath capture_file_path) {
  std::string json_text;
  {
    base::ScopedAllowBlockingForTesting allow_blocking;
    if (!base::ReadFileToString(capture_file_path, &json_text)) {
      VLOG(1) << kTimeClockOverrideNotSetMessage << "Could not read file";
      return false;
    }
  }
  // Decompress the json text from gzip.
  std::string decompressed_json_text;
  if (!compression::GzipUncompress(json_text, &decompressed_json_text)) {
    VLOG(1) << kTimeClockOverrideNotSetMessage
            << "Could not gzip decompress file";
    return false;
  }
  // Convert the file text into a json object.
  std::optional<base::Value> parsed_json = base::JSONReader::Read(
      decompressed_json_text, base::JSON_PARSE_CHROMIUM_EXTENSIONS);
  if (!parsed_json) {
    VLOG(1) << kTimeClockOverrideNotSetMessage << "Failed to deserialize json";
    return false;
  }

  const std::optional<double> time_value =
      parsed_json->GetDict().FindDouble("DeterministicTimeSeedMs");
  if (!time_value) {
    VLOG(1) << kTimeClockOverrideNotSetMessage
            << "No DeterministicTimeSeedMs found";
    return false;
  }

  TimeOverrideHelper::current_time =
      base::Time::FromMillisecondsSinceUnixEpoch(*time_value);
  time_override_ = std::make_unique<base::subtle::ScopedTimeClockOverrides>(
      &TimeOverrideHelper::TimeNow,
      /*time_ticks_override=*/nullptr,
      /*thread_ticks_override=*/nullptr);
  return true;
}

// static
void TestRecipeReplayer::SetUpHostResolverRules(
    base::CommandLine* command_line) {
  // Direct traffic to the Web Page Replay server.
  command_line->AppendSwitchASCII(
      network::switches::kHostResolverRules,
      base::StringPrintf(
          "MAP *:80 127.0.0.1:%d,"
          "MAP *:443 127.0.0.1:%d,"
          // Set to always exclude, allows cache_replayer overwrite
          "EXCLUDE clients1.google.com,"
          "EXCLUDE content-autofill.googleapis.com,"
          "EXCLUDE localhost",
          kHostHttpPort, kHostHttpsPort));
}

// static
void TestRecipeReplayer::SetUpCommandLine(base::CommandLine* command_line) {
  command_line->AppendSwitchASCII(
      network::switches::kIgnoreCertificateErrorsSPKIList,
      kWebPageReplayCertSPKI);
  command_line->AppendSwitch(switches::kStartMaximized);
  // Since we are adding via ScopedFeatureList for test features required, we
  // need to explicitly also enable field trials.
  command_line->AppendSwitch(
      variations::switches::kEnableFieldTrialTestingConfig);
}

TestRecipeReplayChromeFeatureActionExecutor*
TestRecipeReplayer::feature_action_executor() {
  return feature_action_executor_;
}

Browser* TestRecipeReplayer::browser() {
  return browser_;
}

WebPageReplayServerWrapper*
TestRecipeReplayer::web_page_replay_server_wrapper() {
  return web_page_replay_server_wrapper_.get();
}

content::WebContents* TestRecipeReplayer::GetWebContents() {
  return browser_->tab_strip_model()->GetActiveWebContents();
}

namespace {
// Runs the given closure if the `WebContent`'s main frame changes.
class RenderFrameHostChangedObserver : public content::WebContentsObserver {
 public:
  RenderFrameHostChangedObserver(content::WebContents* wc,
                                 base::OnceClosure closure)
      : content::WebContentsObserver(wc), closure_(std::move(closure)) {}
  void RenderFrameHostChanged(content::RenderFrameHost* old_host,
                              content::RenderFrameHost* new_host) override {
    changed_ = true;
    std::move(closure_).Run();
  }
  bool changed() { return changed_; }

 private:
  base::OnceClosure closure_;
  bool changed_ = false;
};
}  // namespace

void TestRecipeReplayer::WaitTillPageIsIdle(
    base::TimeDelta continuous_paint_timeout) {
  // Loop continually while WebContents are waiting for response or loading.
  // page_is_loading is expectedWaitTillPageIsIdle to always got to False
  // eventually, but adding a timeout as a fallback.
  base::TimeTicks finished_load_time = base::TimeTicks::Now();
  while (true) {
    {
      base::RunLoop heart_beat;
      base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
          FROM_HERE, heart_beat.QuitClosure(), wait_for_idle_loop_length);
      heart_beat.Run();
    }
    bool page_is_loading = GetWebContents()->IsWaitingForResponse() ||
                           GetWebContents()->IsLoading();
    if (!page_is_loading)
      break;
    if ((base::TimeTicks::Now() - finished_load_time) >
        continuous_paint_timeout) {
      VLOG(1) << "Page is still loading after "
              << visual_update_timeout.InSeconds()
              << " seconds. Bailing because timeout was reached.";
      break;
    }
  }
  finished_load_time = base::TimeTicks::Now();
  bool frame_changed = false;
  while (true) {
    // Now, rely on the render frame count to be the indicator of page activity.
    // Once all the frames are drawn, we're free to continue.
    content::RenderFrameSubmissionObserver frame_submission_observer(
        GetWebContents());
    base::RunLoop heart_beat;
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE, heart_beat.QuitClosure(), wait_for_idle_loop_length);
    // `frame_submission_observer` holds a reference to metadata from a frame
    // that will be destroyed if a navigation occurs. We need to destroy the
    // observer before the frame is destroyed to avoid a dangling raw
    // pointer. See https://crbug.com/396232961.
    RenderFrameHostChangedObserver frame_observer(GetWebContents(),
                                                  heart_beat.QuitClosure());
    heart_beat.Run();
    if (frame_observer.changed()) {
      VLOG(1) << "The render frame has changed.";
      frame_changed = true;
      break;
    } else if (frame_submission_observer.render_frame_count() == 0) {
      // If the renderer has stopped submitting frames
      VLOG(1) << "The renderer has stopped submitting frames.";
      break;
    } else if ((base::TimeTicks::Now() - finished_load_time) >
               continuous_paint_timeout) {
      // `continuous_paint_timeout` has expired since Chrome loaded the page.
      // During this period of time, Chrome has been continuously painting
      // the page. In this case, the page is probably idle, but a bug, a
      // blinking caret or a persistent animation is keeping the
      // `render_frame_count` from reaching zero. Exit.
      VLOG(1) << "Wait for render frame count timed out after "
              << continuous_paint_timeout.InSeconds()
              << " seconds with the frame count still at: "
              << frame_submission_observer.render_frame_count();
      break;
    }
  }
  // Start waiting on the new frame.
  if (frame_changed) {
    WaitTillPageIsIdle(continuous_paint_timeout);
  }
}

bool TestRecipeReplayer::WaitForVisualUpdate(base::TimeDelta timeout) {
  base::TimeTicks start_time = base::TimeTicks::Now();
  content::RenderFrameSubmissionObserver frame_submission_observer(
      GetWebContents());
  while (frame_submission_observer.render_frame_count() == 0) {
    base::RunLoop heart_beat;
    base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
        FROM_HERE, heart_beat.QuitClosure(), wait_for_idle_loop_length);
    heart_beat.Run();
    if ((base::TimeTicks::Now() - start_time) > timeout) {
      return false;
    }
  }
  return true;
}

void TestRecipeReplayer::CleanupSiteData() {
  // Navigate to about:blank, then clear the browser cache.
  // Navigating to about:blank before clearing the cache ensures that
  // the cleanup is thorough and nothing is held.
  ASSERT_TRUE(
      ui_test_utils::NavigateToURL(browser_, GURL(url::kAboutBlankURL)));
  content::BrowsingDataRemover* remover =
      browser_->GetProfile()->GetBrowsingDataRemover();
  content::BrowsingDataRemoverCompletionObserver completion_observer(remover);
  remover->RemoveAndReply(
      base::Time(), base::Time::Max(),
      content::BrowsingDataRemover::DATA_TYPE_COOKIES,
      content::BrowsingDataRemover::ORIGIN_TYPE_UNPROTECTED_WEB,
      &completion_observer);
  completion_observer.BlockUntilCompletion();
}

bool TestRecipeReplayer::ReplayRecordedActions(
    const base::FilePath& recipe_file_path,
    const std::optional<base::FilePath>& command_file_path) {
  std::optional<base::DictValue> recipe = ReadRecipeFile(recipe_file_path);
  if (!recipe) {
    return false;
  }
  if (!InitializeBrowserToExecuteRecipe(recipe.value())) {
    return false;
  }

  // Iterate through and execute each action in the recipe.
  base::ListValue* action_list = recipe.value().FindList("actions");
  if (!action_list) {
    ADD_FAILURE() << "Failed to extract action list from the recipe!";
    return false;
  }

  ExecutionState execution_state{.length =
                                     static_cast<int>(action_list->size())};
  if (command_file_path.has_value()) {
    execution_state.limit = 0;  // Stop execution initially in debug mode.
    PrintDebugInstructions(command_file_path.value());
  }

  while (execution_state.index < execution_state.length) {
    if (execution_state.pause_on_failure &&
        (testing::Test::HasNonfatalFailure() ||
         testing::Test::HasFatalFailure() || validation_failures_.size() > 0)) {
      // If set to pause on a failure, move limit to current, but then reset
      // `pause_on_failure` so it can continue if the user requests.
      execution_state.limit = execution_state.index;
      execution_state.pause_on_failure = false;
    }
    if (command_file_path.has_value()) {
      while (execution_state.limit <= execution_state.index) {
        // We must call ProcessCommands() on a separate task because it is doing
        // blocking IO. We do busy waiting to block execution until this IO is
        // finished.
        bool thread_finished = false;
        base::ThreadPool::PostTaskAndReplyWithResult(
            FROM_HERE, {base::MayBlock()},
            base::BindOnce(&ProcessCommands, execution_state,
                           std::ref(*action_list), command_file_path.value()),
            base::BindOnce(
                [](ExecutionState* execution_state, bool* finished,
                   ExecutionState new_execution_state) {
                  *execution_state = new_execution_state;
                  *finished = true;
                },
                &execution_state, &thread_finished));
        while (!thread_finished) {
          base::RunLoop run_loop;
          base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
              FROM_HERE, run_loop.QuitClosure(), base::Seconds(1));
          run_loop.Run();
        }
      }
    }
    VLOG(1) << "Proceeding with execution with action " << execution_state.index
            << " of " << execution_state.length << ": "
            << (*action_list)[execution_state.index];

    if (!(*action_list)[execution_state.index].is_dict()) {
      ADD_FAILURE()
          << "Failed to extract an individual action from the recipe!";
      return false;
    }

    base::DictValue action =
        std::move((*action_list)[execution_state.index].GetDict());
    std::optional<std::string> type =
        FindPopulateString(action, "type", "action type");

    if (!type)
      return false;
    if (base::CompareCaseInsensitiveASCII(*type, "autofill") == 0) {
      if (!ExecuteAutofillAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "click") == 0) {
      if (!ExecuteClickAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "clickIfNotSeen") ==
               0) {
      if (!ExecuteClickIfNotSeenAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "closeTab") == 0) {
      if (!ExecuteCloseTabAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "coolOff") == 0) {
      if (!ExecuteCoolOffAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "executeScript") == 0) {
      if (!ExecuteRunCommandAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "hover") == 0) {
      if (!ExecuteHoverAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "loadPage") == 0) {
      if (!ExecuteForceLoadPage(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "pressEnter") == 0) {
      if (!ExecutePressEnterAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "pressEscape") == 0) {
      if (!ExecutePressEscapeAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "pressSpace") == 0) {
      if (!ExecutePressSpaceAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "savePassword") == 0) {
      if (!ExecuteSavePasswordAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "select") == 0) {
      if (!ExecuteSelectDropdownAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "type") == 0) {
      if (!ExecuteTypeAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "typePassword") == 0) {
      if (!ExecuteTypePasswordAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "updatePassword") ==
               0) {
      if (!ExecuteUpdatePasswordAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "validateField") == 0) {
      if (!ExecuteValidateFieldValueAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(
                   *type, "validatePasswordGenerationPrompt") == 0) {
      if (!ExecuteValidatePasswordGenerationPromptAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(
                   *type, "validateNoSavePasswordPrompt") == 0) {
      if (!ExecuteValidateNoSavePasswordPromptAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(
                   *type, "validatePasswordSaveFallback") == 0) {
      if (!ExecuteValidateSaveFallbackAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "waitFor") == 0) {
      if (!ExecuteWaitForStateAction(std::move(action)))
        return false;
    } else if (base::CompareCaseInsensitiveASCII(*type, "breakpoint") == 0) {
      execution_state.limit = execution_state.index + 1;
    } else if (base::CompareCaseInsensitiveASCII(
                   *type, "triggerPasswordChange") == 0) {
      if (!ExecuteTriggerPasswordChangeAction(std::move(action))) {
        return false;
      }
    } else if (base::CompareCaseInsensitiveASCII(
                   *type, "waitForPasswordChangeState") == 0) {
      if (!ExecuteWaitForPasswordChangeStateAction(std::move(action))) {
        return false;
      }
    } else {
      ADD_FAILURE() << "Unrecognized action type: " << *type;
    }

    ++execution_state.index;
  }

  return true;
}

// Functions for deserializing and executing actions from the test recipe
// JSON object.
bool TestRecipeReplayer::InitializeBrowserToExecuteRecipe(
    base::DictValue& recipe) {
  // Setup any saved address and credit card at the start of the test.
  auto* autofill_profile_container = recipe.Find("autofillProfile");

  if (autofill_profile_container) {
    if (!autofill_profile_container->is_list()) {
      ADD_FAILURE() << "Save Autofill Profile is not a list!";
      return false;
    }

    if (!SetupSavedAutofillProfile(
            std::move(*autofill_profile_container).TakeList())) {
      return false;
    }
  }

  // Setup any saved passwords at the start of the test.
  auto* saved_password_container = recipe.Find("passwordManagerProfiles");

  if (saved_password_container) {
    if (!saved_password_container->is_list()) {
      ADD_FAILURE() << "Saved Password List is not a list!";
      return false;
    }

    if (!SetupSavedPasswords(std::move(*saved_password_container).TakeList())) {
      return false;
    }
  }

  // Extract the starting URL from the test recipe.
  auto* starting_url = recipe.FindString("startingURL");
  if (!starting_url) {
    ADD_FAILURE() << "Failed to extract the starting url from the recipe!";
    return false;
  }

  // Navigate to the starting URL, wait for the page to complete loading.
  if (!content::ExecJs(GetWebContents(),
                       base::StringPrintf("window.location.href = '%s';",
                                          starting_url->c_str()))) {
    ADD_FAILURE() << "Failed to navigate Chrome to '" << *starting_url << "!";
    return false;
  }

  WaitTillPageIsIdle();
  return true;
}

bool TestRecipeReplayer::ExecuteAutofillAction(base::DictValue action) {
  std::string xpath;
  content::RenderFrameHost* frame;
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame))
    return false;
  std::vector<std::string> frame_path;
  if (!GetIFramePathFromAction(action, &frame_path))
    return false;

  VLOG(1) << "Invoking Chrome Autofill on `" << xpath << "`.";
  // Clear the input box first, in case a previous value is there.
  // If the text input box is not clear, pressing the down key will not
  // bring up the autofill suggestion box.
  // This can happen on sites that requires the user to sign in. After
  // signing in, the site fills the form with the user's profile
  // information.
  if (!ExecuteJavaScriptOnElementByXpath(
          frame, xpath,
          "automation_helper.setInputElementValue(target, ``);")) {
    ADD_FAILURE() << "Failed to clear the input field value!";
    return false;
  }

  std::string autofill_triggered_field_type;
  if (GetElementProperty(frame, xpath,
                         "return target.getAttribute('autofill-prediction');",
                         &autofill_triggered_field_type)) {
    VLOG(1) << "The field's Chrome Autofill annotation: "
            << autofill_triggered_field_type << " during autofill form step.";
  } else {
    VLOG(1) << "Failed to obtain the field's Chrome Autofill annotation during "
               "autofill form step!";
  }
  if (!feature_action_executor()->AutofillForm(
          xpath, frame_path, kAutofillActionNumRetries, frame,
          StringToFieldType(autofill_triggered_field_type))) {
    return false;
  }
  WaitTillPageIsIdle(kAutofillActionWaitForVisualUpdateTimeout);
  return true;
}

bool TestRecipeReplayer::ExecuteClickAction(base::DictValue action) {
  std::string xpath;
  content::RenderFrameHost* frame;
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame)) {
    return false;
  }

  VLOG(1) << "Left mouse clicking `" << xpath << "`.";
  if (!ScrollElementIntoView(xpath, frame)) {
    return false;
  }
  WaitTillPageIsIdle(scroll_wait_timeout);
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame)) {
    return false;
  }

  gfx::Rect rect;
  if (!GetBoundingRectOfTargetElement(xpath, frame, &rect)) {
    return false;
  }
  if (!SimulateLeftMouseClickAt(rect.CenterPoint(), frame)) {
    return false;
  }

  WaitTillPageIsIdle();
  return true;
}

bool TestRecipeReplayer::ExecuteClickIfNotSeenAction(base::DictValue action) {
  std::string xpath;
  content::RenderFrameHost* frame;
  if (ExtractFrameAndVerifyElement(action, &xpath, &frame, false, false,
                                   true)) {
    return true;
  } else {
    // If the selector wasn't found, take the clickSelector and make it the
    // selector to attempt a click with that element instead.
    std::optional<std::string> click_xpath_text =
        FindPopulateString(action, "clickSelector", "click xpath selector");

    action.Set("selector", *click_xpath_text);

    return ExecuteClickAction(std::move(action));
  }
}

bool TestRecipeReplayer::ExecuteCloseTabAction(base::DictValue action) {
  VLOG(1) << "Closing Active Tab";
  browser_->tab_strip_model()->CloseSelectedTabs();
  return true;
}

bool TestRecipeReplayer::ExecuteCoolOffAction(base::DictValue action) {
  base::RunLoop heart_beat;
  base::TimeDelta cool_off_time = cool_off_action_timeout;
  base::Value* pause_time_container = action.Find("pauseTimeSec");
  if (pause_time_container) {
    if (!pause_time_container->is_int()) {
      ADD_FAILURE() << "Pause time is not an integer!";
      return false;
    }
    int seconds = pause_time_container->GetInt();
    cool_off_time = base::Seconds(seconds);
  }
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostDelayedTask(
      FROM_HERE, heart_beat.QuitClosure(), cool_off_time);
  VLOG(1) << "Pausing execution for '" << cool_off_time.InSeconds()
          << "' seconds";
  heart_beat.Run();

  return true;
}

bool TestRecipeReplayer::ExecuteHoverAction(base::DictValue action) {
  std::string xpath;
  content::RenderFrameHost* frame;
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame)) {
    return false;
  }

  VLOG(1) << "Hovering over `" << xpath << "`.";

  if (!ScrollElementIntoView(xpath, frame)) {
    return false;
  }
  WaitTillPageIsIdle(scroll_wait_timeout);
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame)) {
    return false;
  }

  gfx::Rect rect;
  if (!GetBoundingRectOfTargetElement(xpath, frame, &rect)) {
    return false;
  }

  if (!SimulateMouseHoverAt(frame, rect.CenterPoint())) {
    return false;
  }

  if (!WaitForVisualUpdate()) {
    ADD_FAILURE() << "The page did not respond to a mouse hover action!";
    return false;
  }

  return true;
}

bool TestRecipeReplayer::ExecuteForceLoadPage(base::DictValue action) {
  bool should_force = action.FindBool("force").value_or(false);
  if (!should_force) {
    return true;
  }

  std::optional<std::string> url =
      FindPopulateString(action, "url", "Force Load URL");
  if (!url)
    return false;
  VLOG(1) << "Making explicit URL redirect to '" << *url << "'";
  EXPECT_TRUE(ui_test_utils::NavigateToURL(browser_, GURL(*url)));

  WaitTillPageIsIdle();

  return true;
}

bool TestRecipeReplayer::ExecutePressEnterAction(base::DictValue action) {
  std::string xpath;
  content::RenderFrameHost* frame;
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame))
    return false;

  VLOG(1) << "Pressing 'Enter' on `" << xpath << "`.";
  SimulateKeyPressWrapper(content::WebContents::FromRenderFrameHost(frame),
                          ui::DomKey::ENTER);
  WaitTillPageIsIdle();
  return true;
}

bool TestRecipeReplayer::ExecutePressEscapeAction(base::DictValue action) {
  VLOG(1) << "Pressing 'Esc' in the current frame";
  SimulateKeyPressWrapper(GetWebContents(), ui::DomKey::ESCAPE);
  WaitTillPageIsIdle();
  return true;
}

bool TestRecipeReplayer::ExecutePressSpaceAction(base::DictValue action) {
  std::string xpath;
  content::RenderFrameHost* frame;
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame, true))
    return false;

  VLOG(1) << "Pressing 'Space' on `" << xpath << "`.";
  SimulateKeyPressWrapper(content::WebContents::FromRenderFrameHost(frame),
                          ui::DomKey::FromCharacter(' '));
  WaitTillPageIsIdle();
  return true;
}

bool TestRecipeReplayer::ExecuteRunCommandAction(base::DictValue action) {
  // Extract the list of JavaScript commands into a vector.
  std::vector<std::string> commands;

  base::ListValue* list = action.FindList("commands");
  if (!list) {
    ADD_FAILURE() << "Failed to extract commands list from action";
    return false;
  }

  for (const auto& command : *list) {
    if (!command.is_string()) {
      ADD_FAILURE() << "command is not a string: " << command;
      return false;
    }
    commands.push_back(command.GetString());
  }

  content::RenderFrameHost* frame;
  if (!GetTargetFrameFromAction(action, &frame)) {
    return false;
  }

  VLOG(1) << "Running JavaScript commands on the page.";

  // Execute the commands.
  for (const std::string& command : commands) {
    if (!content::ExecJs(frame, command)) {
      ADD_FAILURE() << "Failed to execute JavaScript command `" << command
                    << "`!";
      return false;
    }
    // Wait in case the JavaScript command triggers page load or layout
    // changes.
    WaitTillPageIsIdle();
  }

  return true;
}

bool TestRecipeReplayer::ExecuteSavePasswordAction(base::DictValue action) {
  VLOG(1) << "Save password.";

  if (!feature_action_executor()->SavePassword())
    return false;

  bool stored_cred;
  if (!HasChromeStoredCredential(action, &stored_cred))
    return false;

  if (!stored_cred) {
    ADD_FAILURE() << "Chrome did not save the credential!";
    return false;
  }

  return true;
}

bool TestRecipeReplayer::ExecuteSelectDropdownAction(base::DictValue action) {
  std::optional<int> index = action.FindInt("index");
  if (!index.has_value()) {
    ADD_FAILURE() << "Failed to extract Selection Index from action";
    return false;
  }

  std::string xpath;
  content::RenderFrameHost* frame;
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame))
    return false;

  VLOG(1) << "Select option '" << *index << "' from `" << xpath << "`.";
  if (!ExecuteJavaScriptOnElementByXpath(
          frame, xpath,
          base::StringPrintf(
              "automation_helper"
              "  .selectOptionFromDropDownElementByIndex(target, %d);",
              *index))) {
    ADD_FAILURE() << "Failed to select drop down option with JavaScript!";
    return false;
  }
  WaitTillPageIsIdle();
  return true;
}

bool TestRecipeReplayer::ExecuteTypeAction(base::DictValue action) {
  std::optional<std::string> value =
      FindPopulateString(action, "value", "typing value");
  if (!value)
    return false;

  std::string xpath;
  content::RenderFrameHost* frame;
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame))
    return false;

  VLOG(1) << "Typing '" << *value << "' inside `" << xpath << "`.";
  if (!ExecuteJavaScriptOnElementByXpath(
          frame, xpath,
          base::StringPrintf(
              "automation_helper.setInputElementValue(target, `%s`);",
              value->c_str()))) {
    ADD_FAILURE() << "Failed to type inside input element with JavaScript!";
    return false;
  }
  WaitTillPageIsIdle();
  return true;
}

bool TestRecipeReplayer::ExecuteTypePasswordAction(base::DictValue action) {
  std::string xpath;
  content::RenderFrameHost* frame;
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame, true))
    return false;

  std::optional<std::string> value =
      FindPopulateString(action, "value", "password text");
  if (!value)
    return false;

  // Clear the password field first, in case a previous value is there.
  if (!ExecuteJavaScriptOnElementByXpath(
          frame, xpath,
          "automation_helper.setInputElementValue(target, ``);")) {
    ADD_FAILURE() << "Failed to execute JavaScript to clear the input value!";
    return false;
  }

  VLOG(1) << "Typing '" << *value << "' inside `" << xpath << "`.";
  content::WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(frame);
  for (size_t index = 0; index < value->size(); index++) {
    SimulateKeyPressWrapper(web_contents,
                            ui::DomKey::FromCharacter(value->at(index)));
  }
  WaitTillPageIsIdle();
  return true;
}

bool TestRecipeReplayer::ExecuteUpdatePasswordAction(base::DictValue action) {
  VLOG(1) << "Update password.";

  if (!feature_action_executor()->UpdatePassword())
    return false;

  bool stored_cred;
  if (!HasChromeStoredCredential(action, &stored_cred))
    return false;

  if (!stored_cred) {
    ADD_FAILURE() << "Chrome did not update the credential!";
    return false;
  }

  return true;
}

bool TestRecipeReplayer::ExecuteValidateFieldValueAction(
    base::DictValue action) {
  std::string xpath;
  content::RenderFrameHost* frame;
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame, false, true))
    return false;

  base::Value* autofill_prediction_container =
      action.Find("expectedAutofillType");
  if (autofill_prediction_container) {
    if (!autofill_prediction_container->is_string()) {
      ADD_FAILURE() << "Autofill prediction is not a string!";
      return false;
    }

    // If we are validating the value of a Chrome autofilled field, print the
    // Chrome Autofill's field annotation for debugging purpose.
    std::string autofill_information;
    if (GetElementProperty(
            frame, xpath, "return target.getAttribute('autofill-information');",
            &autofill_information)) {
      VLOG(1) << autofill_information;
    } else {
      // Only used for logging purposes, so don't ADD_FAILURE() if it fails.
      VLOG(1) << "Failed to obtain the field's Chrome Autofill annotation!";
    }

    std::string expected_autofill_prediction_type =
        autofill_prediction_container->GetString();
    VLOG(1) << "Checking the field `" << xpath << "` has the autofill type '"
            << expected_autofill_prediction_type << "'";
    ExpectElementPropertyEqualsAnyOf(
        frame, xpath, "return target.getAttribute('autofill-prediction');",
        {expected_autofill_prediction_type}, "autofill type mismatch",
        IgnoreCase(true));
  }

  std::optional<std::vector<std::string>> expected_values =
      FindPopulateStringVector(action, "expectedValues", AllowNull());
  std::optional<std::string> expected_value =
      FindPopulateString(action, "expectedValue", AllowNull());
  if (!!expected_values == !!expected_value) {
    ADD_FAILURE() << "Failed to extract 'expectedValue' xor 'expectedValues' "
                     "vector from container !";
    return false;
  }

  VLOG(1) << "Checking the field `" << xpath << "`.";
  if (expected_value) {
    ExpectElementPropertyEqualsAnyOf(frame, xpath, "return target.value;",
                                     {*expected_value}, "text value mismatch");
  }
  if (expected_values) {
    ExpectElementPropertyEqualsAnyOf(frame, xpath, "return target.value;",
                                     *expected_values, "text value mismatch");
  }
  return true;
}

bool TestRecipeReplayer::ExecuteValidateNoSavePasswordPromptAction(
    base::DictValue action) {
  VLOG(1) << "Verify that the page hasn't shown a save password prompt.";
  EXPECT_FALSE(feature_action_executor()->HasChromeShownSavePasswordPrompt());
  return true;
}

bool TestRecipeReplayer::ExecuteTriggerPasswordChangeAction(
    base::DictValue action) {
  std::optional<std::string> url =
      FindPopulateString(action, "change_password_url", "Change Password URL");
  if (!url) {
    return false;
  }

  feature_action_executor()->TriggerPasswordChange(GURL(url.value()));
  return true;
}

bool TestRecipeReplayer::ExecuteWaitForPasswordChangeStateAction(
    base::DictValue action) {
  int expected_state = action.FindInt("state").value_or(0);

  feature_action_executor()->WaitForPasswordChangeState(expected_state);
  return true;
}

bool TestRecipeReplayer::ExecuteValidatePasswordGenerationPromptAction(
    base::DictValue action) {
  VLOG(1) << "Verify that an element is properly displaying or not displaying "
             "the password generation prompt";
  std::string xpath;
  content::RenderFrameHost* frame;
  if (!ExtractFrameAndVerifyElement(action, &xpath, &frame, true))
    return false;

  // Most common scenario is validating that the password generation prompt is
  // being shown, so if unspecified default to true.
  bool expect_to_be_shown = expect_to_be_shown =
      action.FindBool("shouldBeShown").value_or(true);

  // First, execute a click to focus on the field in question.
  ExecuteClickAction(std::move(action));

  // Validate that the password generation prompt is shown when expected.
  ValidatePasswordGenerationPromptState(frame, xpath, expect_to_be_shown);
  return true;
}

void TestRecipeReplayer::ValidatePasswordGenerationPromptState(
    const content::ToRenderFrameHost& frame,
    const std::string& element_xpath,
    bool expect_to_be_shown) {
  bool is_password_generation_prompt_showing =
      feature_action_executor()->IsChromeShowingPasswordGenerationPrompt();

  if (expect_to_be_shown != is_password_generation_prompt_showing) {
    std::string error_message =
        base::StrCat({"Field xpath: `", element_xpath, "` is",
                      is_password_generation_prompt_showing ? "" : " not",
                      " showing password generation prompt but it should",
                      expect_to_be_shown ? "" : " not", " be."});
    VLOG(1) << error_message;
    ADD_FAILURE() << error_message;
    validation_failures_.push_back(testing::AssertionFailure()
                                   << error_message);
  }
}

bool TestRecipeReplayer::ExecuteValidateSaveFallbackAction(
    base::DictValue action) {
  VLOG(1) << "Verify that Chrome shows the save fallback icon in the omnibox.";
  EXPECT_TRUE(feature_action_executor()->WaitForSaveFallback());
  return true;
}

bool TestRecipeReplayer::ExecuteWaitForStateAction(base::DictValue action) {
  // Extract the list of JavaScript assertions into a vector.
  std::vector<std::string> state_assertions;

  base::ListValue* list = action.FindList("assertions");
  if (!list) {
    ADD_FAILURE() << "Failed to extract wait assertions list from action";
    return false;
  }
  for (const base::Value& assertion : *list) {
    if (!assertion.is_string()) {
      ADD_FAILURE() << "Assertion is not a string: " << assertion;
      return false;
    }
    state_assertions.push_back(assertion.GetString());
  }

  content::RenderFrameHost* frame;
  VLOG(1) << "Waiting for page to reach a state.";

  // Wait for all of the assertions to become true on the current page.
  return WaitForStateChange(action, &frame, state_assertions,
                            default_action_timeout);
}

bool TestRecipeReplayer::GetTargetHTMLElementXpathFromAction(
    const base::DictValue& action,
    std::string* xpath) {
  xpath->clear();
  std::optional<std::string> xpath_text =
      FindPopulateString(action, "selector", "xpath selector");
  if (!xpath_text)
    return false;
  *xpath = *xpath_text;
  return true;
}

bool TestRecipeReplayer::GetTargetHTMLElementVisibilityEnumFromAction(
    const base::DictValue& action,
    int* visibility_enum_val) {
  const base::Value* visibility_container = action.Find("visibility");
  if (!visibility_container) {
    // By default, set the visibility to (visible | enabled | on_top), as
    // defined in
    // chrome/test/data/web_page_replay_go_helper_scripts/automation_helper.js
    *visibility_enum_val = 7;
    return true;
  }

  if (!visibility_container->is_int()) {
    ADD_FAILURE() << "visibility property is not an integer!";
    return false;
  }

  *visibility_enum_val = visibility_container->GetInt();
  return true;
}

bool TestRecipeReplayer::GetTargetFrameFromAction(
    const base::DictValue& action,
    content::RenderFrameHost** frame) {
  const base::Value* iframe_container = action.Find("context");
  if (!iframe_container) {
    ADD_FAILURE() << "Failed to extract the iframe context from action!";
    return false;
  }

  if (!iframe_container->is_dict()) {
    ADD_FAILURE() << "Failed to extract the iframe context object!";
    return false;
  }

  std::optional<bool> is_iframe_container =
      iframe_container->GetDict().FindBool("isIframe");
  if (!is_iframe_container) {
    ADD_FAILURE() << "Failed to extract isIframe from the iframe context! ";
    return false;
  }
  if (!is_iframe_container.value_or(false)) {
    *frame = GetWebContents()->GetPrimaryMainFrame();
    return true;
  }

  const base::Value* frame_name_container =
      iframe_container->GetDict().FindByDottedPath("browserTest.name");
  const base::Value* frame_origin_container =
      iframe_container->GetDict().FindByDottedPath("browserTest.origin");
  const base::Value* frame_url_container =
      iframe_container->GetDict().FindByDottedPath("browserTest.url");
  if (frame_name_container != nullptr && !frame_name_container->is_string()) {
    ADD_FAILURE() << "Iframe name is not a string!";
    return false;
  }

  if (frame_origin_container != nullptr &&
      !frame_origin_container->is_string()) {
    ADD_FAILURE() << "Iframe origin is not a string!";
    return false;
  }

  if (frame_url_container != nullptr && !frame_url_container->is_string()) {
    ADD_FAILURE() << "Iframe url is not a string!";
    return false;
  }

  if (frame_name_container != nullptr) {
    std::string frame_name = frame_name_container->GetString();
    *frame = WaitForFrameMatchingName(*GetWebContents(), frame_name);
  } else if (frame_origin_container != nullptr) {
    std::string frame_origin = frame_origin_container->GetString();
    *frame = WaitForFrameMatchingOrigin(
        *GetWebContents(), url::SchemeHostPort(GURL(frame_origin)));
  } else if (frame_url_container != nullptr) {
    std::string frame_url = frame_url_container->GetString();
    *frame = WaitForFrameMatchingUrl(*GetWebContents(), GURL(frame_url));
  } else {
    ADD_FAILURE() << "The recipe does not specify a way to find the iframe!";
  }

  if (*frame == nullptr) {
    ADD_FAILURE() << "Failed to find iframe!";
    return false;
  }

  return true;
}

bool TestRecipeReplayer::ExtractFrameAndVerifyElement(
    const base::DictValue& action,
    std::string* xpath,
    content::RenderFrameHost** frame,
    bool set_focus,
    bool relaxed_visibility,
    bool ignore_failure) {
  if (!GetTargetHTMLElementXpathFromAction(action, xpath)) {
    return false;
  }

  int visibility_enum_val;
  if (!GetTargetHTMLElementVisibilityEnumFromAction(action,
                                                    &visibility_enum_val)) {
    return false;
  }

  // If we're just validating we don't care about on_top-ness, as copied from
  // chrome/test/data/web_page_replay_go_helper_scripts/automation_helper.js
  // to TestRecipeReplayer::DomElementReadyState enum
  // So remove (DomElementReadyState::kReadyStateOnTop)
  if (relaxed_visibility) {
    visibility_enum_val &= ~kReadyStateOnTop;
  }

  if (!WaitForElementToBeReady(*xpath, visibility_enum_val, action, frame,
                               ignore_failure)) {
    return false;
  }

  if (set_focus) {
    std::vector<std::string> frame_path;
    if (!GetIFramePathFromAction(action, &frame_path))
      return false;

    if (!PlaceFocusOnElement(*xpath, frame_path, *frame))
      return false;
  }
  return true;
}

bool TestRecipeReplayer::GetIFramePathFromAction(
    const base::DictValue& action,
    std::vector<std::string>* iframe_path) {
  *iframe_path = std::vector<std::string>();

  const base::Value* iframe_container = action.Find("context");
  if (!iframe_container) {
    ADD_FAILURE() << "Failed to extract the iframe context from action!";
    return false;
  }

  if (!iframe_container->is_dict()) {
    ADD_FAILURE() << "Failed to extract the iframe context object!";
    return false;
  }

  const base::Value* iframe_path_container =
      iframe_container->GetDict().Find("path");
  if (!iframe_path_container) {
    // If the action does not have a path container, it would mean that:
    // 1. The target frame is the top level frame.
    // 2. The target frame is an iframe, but it is the top-level frame in its
    //    rendering process.
    return true;
  }
  if (!iframe_path_container->is_list()) {
    ADD_FAILURE() << "The action's iframe path is not a list!";
    return false;
  }
  for (const auto& xpath : iframe_path_container->GetList()) {
    if (!xpath.is_string()) {
      ADD_FAILURE() << "Failed to extract the iframe xpath from action!";
      return false;
    }
    iframe_path->push_back(xpath.GetString());
  }
  return true;
}

bool TestRecipeReplayer::GetIFrameOffsetFromIFramePath(
    const std::vector<std::string>& iframe_path,
    content::RenderFrameHost* frame,
    gfx::Vector2d* offset) {
  *offset = gfx::Vector2d(0, 0);

  for (auto it_xpath = iframe_path.begin(); it_xpath != iframe_path.end();
       it_xpath++) {
    content::RenderFrameHost* parent_frame = frame->GetParent();
    if (parent_frame == nullptr) {
      ADD_FAILURE() << "Trying to iterate past the top level frame!";
      return false;
    }

    gfx::Rect rect;
    if (!GetBoundingRectOfTargetElement(*it_xpath, parent_frame, &rect)) {
      ADD_FAILURE() << "Failed to extract position of iframe with xpath `"
                    << *it_xpath << "`!";
      return false;
    }

    *offset += rect.OffsetFromOrigin();
    frame = parent_frame;
  }

  return true;
}

bool TestRecipeReplayer::WaitForElementToBeReady(
    const std::string& xpath,
    const int visibility_enum_val,
    const base::DictValue& action,
    content::RenderFrameHost** frame,
    bool ignore_failure) {
  std::vector<std::string> state_assertions;
  state_assertions.push_back(base::StringPrintf(
      "return automation_helper.isElementWithXpathReady(`%s`, %d);",
      xpath.c_str(), visibility_enum_val));
  return WaitForStateChange(
      action, frame, state_assertions,
      ignore_failure ? click_fallback_timeout : default_action_timeout,
      ignore_failure);
}

bool TestRecipeReplayer::WaitForStateChange(
    const base::DictValue& action,
    content::RenderFrameHost** frame,
    const std::vector<std::string>& state_assertions,
    const base::TimeDelta& timeout,
    bool ignore_failure) {
  base::TimeTicks start_time = base::TimeTicks::Now();
  while (true) {
    if (!GetTargetFrameFromAction(action, frame)) {
      return false;
    }
    if (AllAssertionsPassed(*frame, state_assertions)) {
      return true;
    }
    if (base::TimeTicks::Now() - start_time > timeout) {
      if (!ignore_failure) {
        ADD_FAILURE() << "State change hasn't completed within timeout.";
      }
      return false;
    }
    WaitTillPageIsIdle();
  }
}

bool TestRecipeReplayer::AllAssertionsPassed(
    const content::ToRenderFrameHost& frame,
    const std::vector<std::string>& assertions) {
  // We may be dealing with a frame that is mid-/post-navigation. `EvalJs`
  // will fail if the frame is e.g. unloading or in back/forward-cache. Don't
  // even try unless the frame is active.
  if (frame.render_frame_host()->GetLifecycleState() !=
      content::RenderFrameHost::LifecycleState::kActive) {
    VLOG(1) << "Frame not active, not testing assertions. "
            << std::to_underlying(
                   frame.render_frame_host()->GetLifecycleState());
    return false;
  }
  for (const std::string& assertion : assertions) {
    content::EvalJsResult result =
        EvalJs(frame, base::StringPrintf("(function() {"
                                         "  try {"
                                         "    %s"
                                         "  } catch (ex) {}"
                                         "  return false;"
                                         "})();",
                                         assertion.c_str()));
    if (!result.is_ok()) {
      VLOG(1) << "'" << assertion << "' failed: " << result.ExtractError();
      return false;
    }
    if (!result.is_bool()) {
      VLOG(1) << "'" << assertion << "' failed: Did not return boolean.";
      return false;
    }
    if (!result.ExtractBool()) {
      VLOG(1) << "'" << assertion << "' failed: Returned false.";
      return false;
    }
  }
  return true;
}

bool TestRecipeReplayer::ExecuteJavaScriptOnElementByXpath(
    const content::ToRenderFrameHost& frame,
    const std::string& element_xpath,
    const std::string& execute_function_body,
    const base::TimeDelta& time_to_wait_for_element) {
  if (!frame.render_frame_host() || !frame.render_frame_host()->IsActive()) {
    return false;
  }
  std::string js(base::StringPrintf(
      "try {"
      "  var element = automation_helper.getElementByXpath(`%s`);"
      "  (function(target) { %s })(element);"
      "} catch(ex) {}",
      element_xpath.c_str(), execute_function_body.c_str()));
  return ExecJs(frame, js);
}

bool TestRecipeReplayer::GetElementProperty(
    const content::ToRenderFrameHost& frame,
    const std::string& element_xpath,
    const std::string& get_property_function_body,
    std::string* property) {
  content::EvalJsResult result = content::EvalJs(
      frame, base::StringPrintf(
                 "(function() {"
                 "    var element = function() {"
                 "      return automation_helper.getElementByXpath(`%s`);"
                 "    }();"
                 "    return function(target){%s}(element);})();",
                 element_xpath.c_str(), get_property_function_body.c_str()));
  if (result.is_string()) {
    *property = result.ExtractString();
    return true;
  }
  *property = result.is_ok() ? "" : result.ExtractError();
  return false;
}

bool TestRecipeReplayer::ExpectElementPropertyEqualsAnyOf(
    const content::ToRenderFrameHost& frame,
    const std::string& element_xpath,
    const std::string& get_property_function_body,
    const std::vector<std::string>& expected_values,
    const std::string& validation_field,
    IgnoreCase ignore_case) {
  std::string actual_value;
  if (!GetElementProperty(frame, element_xpath, get_property_function_body,
                          &actual_value)) {
    ADD_FAILURE() << "Failed to extract element property! " << element_xpath
                  << ", " << get_property_function_body;
    return false;
  }

  auto is_expected = [ignore_case,
                      &actual_value](std::string_view expected_value) {
    return ignore_case
               ? base::EqualsCaseInsensitiveASCII(expected_value, actual_value)
               : expected_value == actual_value;
  };

  if (std::ranges::none_of(expected_values, is_expected)) {
    std::string error_message = base::StrCat(
        {"Field xpath: `", element_xpath, "` ", validation_field, ", ",
         "Expected: '", base::JoinString(expected_values, " or "),
         "', actual: '", actual_value, "'"});
    VLOG(1) << error_message;
    validation_failures_.push_back(testing::AssertionFailure()
                                   << error_message);
  }
  return true;
}

bool TestRecipeReplayer::ScrollElementIntoView(
    const std::string& element_xpath,
    content::RenderFrameHost* frame) {
  if (!frame || !frame->IsActive()) {
    return false;
  }
  const std::string scroll_target_js(base::StringPrintf(
      "try {"
      "  const element = automation_helper.getElementByXpath(`%s`);"
      "  element.scrollIntoView({"
      "    block: 'center', inline: 'center'});"
      "  true;"
      "} catch(ex) {"
      "  false;"
      "}",
      element_xpath.c_str()));

  content::EvalJsResult result = EvalJs(frame, scroll_target_js);
  if (result.is_bool()) {
    return result.ExtractBool();
  }
  return false;
}

bool TestRecipeReplayer::PlaceFocusOnElement(
    const std::string& element_xpath,
    const std::vector<std::string>& iframe_path,
    content::RenderFrameHost* frame) {
  if (!frame || !frame->IsActive()) {
    return false;
  }
  if (!ScrollElementIntoView(element_xpath, frame))
    return false;

  const std::string focus_on_target_field_js(
      base::StringPrintf("(function() {const element = "
                         "automation_helper.getElementByXpath(`%s`);"
                         "    if (document.activeElement !== element) {"
                         "      element.focus();"
                         "    }"
                         "    return document.activeElement === element;})();",
                         element_xpath.c_str()));

  content::EvalJsResult result =
      content::EvalJs(frame, focus_on_target_field_js);
  if (result.is_bool() && result.ExtractBool()) {
    return true;
  }

  VLOG(1) << "Failed to focus element through script:"
          << (result.is_ok()
                  ? (result.is_bool() ? "Returned false" : "Not a valid bool")
                  : result.ExtractError());

  // Failing focusing on an element through script, use the less preferred
  // method of left mouse clicking the element.
  gfx::Rect rect;
  if (!GetBoundingRectOfTargetElement(element_xpath, iframe_path, frame,
                                      &rect)) {
    return false;
  }

  return SimulateLeftMouseClickAt(rect.CenterPoint(), frame);
}

bool TestRecipeReplayer::GetBoundingRectOfTargetElement(
    const std::string& target_element_xpath,
    content::RenderFrameHost* frame,
    gfx::Rect* output_rect) {
  if (!frame || !frame->IsActive()) {
    return false;
  }
  const std::string get_element_bounding_rect_js(base::StringPrintf(
      "(function() {"
      "   try {"
      "     const element = automation_helper.getElementByXpath(`%s`);"
      "     const rect = element.getBoundingClientRect();"
      "     return Math.round(rect.left) + ',' + "
      "            Math.round(rect.top) + ',' + "
      "            Math.round(rect.width) + ',' + "
      "            Math.round(rect.height);"
      "   } catch(ex) {}"
      "   return '';"
      "})();",
      target_element_xpath.c_str()));

  content::EvalJsResult result =
      content::EvalJs(frame, get_element_bounding_rect_js);
  if (!result.is_string()) {
    return false;
  }

  std::string rect_str = result.ExtractString();

  if (rect_str.empty()) {
    if (frame->IsActive()) {
      ADD_FAILURE() << "Failed to extract target element's bounding rect!";
    }
    return false;
  }

  // Parse the bounding rect string to extract the element coordinates.
  std::vector<std::string_view> rect_components = base::SplitStringPiece(
      rect_str, ",", base::KEEP_WHITESPACE, base::SPLIT_WANT_ALL);
  if (rect_components.size() != 4) {
    ADD_FAILURE() << "Wrong number of components in `" << rect_str << "`!";
    return false;
  }

  int x = 0;
  if (!base::StringToInt(rect_components[0], &x)) {
    ADD_FAILURE() << "Failed to extract target element's x coordinate from "
                  << "the string `" << rect_components[0] << "`!";
    return false;
  }
  int y = 0;
  if (!base::StringToInt(rect_components[1], &y)) {
    ADD_FAILURE() << "Failed to extract target element's y coordinate from "
                  << "the string `" << rect_components[1] << "`!";
    return false;
  }
  int width = 0;
  if (!base::StringToInt(rect_components[2], &width)) {
    ADD_FAILURE() << "Failed to extract target element's width from "
                  << "the string `" << rect_components[2] << "`!";
    return false;
  }
  int height = 0;
  if (!base::StringToInt(rect_components[3], &height)) {
    ADD_FAILURE() << "Failed to extract target element's height from "
                  << "the string `" << rect_components[3] << "`!";
    return false;
  }

  output_rect->set_x(x);
  output_rect->set_y(y);
  output_rect->set_width(width);
  output_rect->set_height(height);

  return true;
}

bool TestRecipeReplayer::GetBoundingRectOfTargetElement(
    const std::string& target_element_xpath,
    const std::vector<std::string>& iframe_path,
    content::RenderFrameHost* frame,
    gfx::Rect* output_rect) {
  if (!frame || !frame->IsActive()) {
    return false;
  }
  gfx::Vector2d offset;
  if (!GetIFrameOffsetFromIFramePath(iframe_path, frame, &offset))
    return false;
  if (!GetBoundingRectOfTargetElement(target_element_xpath, frame, output_rect))
    return false;

  *output_rect += offset;
  return true;
}

bool TestRecipeReplayer::SimulateLeftMouseClickAt(
    const gfx::Point& point,
    content::RenderFrameHost* render_frame_host) {
  if (!render_frame_host || !render_frame_host->IsActive()) {
    return false;
  }

  content::RenderFrameHostWrapper rfh_wrapper(render_frame_host);
  if (!SimulateMouseHoverAt(render_frame_host, point)) {
    return false;
  }

  if (rfh_wrapper.IsDestroyed() || !rfh_wrapper.get()->IsActive()) {
    return false;
  }

  render_frame_host = rfh_wrapper.get();
  content::RenderWidgetHostView* view = render_frame_host->GetView();
  content::WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(render_frame_host);
  if (!view || !web_contents) {
    return false;
  }

  content::RenderWidgetHost* widget = view->GetRenderWidgetHost();
  if (!widget) {
    return false;
  }

  blink::WebMouseEvent mouse_event(
      blink::WebInputEvent::Type::kMouseDown,
      blink::WebInputEvent::kNoModifiers,
      blink::WebInputEvent::GetStaticTimeStampForTests());
  mouse_event.button = blink::WebMouseEvent::Button::kLeft;
  mouse_event.SetPositionInWidget(point.x(), point.y());
  mouse_event.SetTimeStamp(base::TimeTicks::Now());

  // Mac needs positionInScreen for events to plugins.
  gfx::Rect offset = web_contents->GetContainerBounds();
  mouse_event.SetPositionInScreen(point.x() + offset.x(),
                                  point.y() + offset.y());
  mouse_event.click_count = 1;

  widget->ForwardMouseEvent(mouse_event);
  mouse_event.SetType(blink::WebInputEvent::Type::kMouseUp);
  widget->ForwardMouseEvent(mouse_event);
  return true;
}

bool TestRecipeReplayer::SimulateMouseHoverAt(
    content::RenderFrameHost* render_frame_host,
    const gfx::Point& point) {
  if (!render_frame_host || !render_frame_host->IsActive()) {
    return false;
  }
  content::WebContents* web_contents =
      content::WebContents::FromRenderFrameHost(render_frame_host);
  if (!web_contents) {
    return false;
  }
  gfx::Rect offset = web_contents->GetContainerBounds();
  gfx::Point reset_mouse =
      gfx::Point(offset.x() + point.x(), offset.y() + point.y());
  if (!ui_test_utils::SendMouseMoveSync(reset_mouse)) {
    ADD_FAILURE() << "Failed to position the mouse!";
    return false;
  }
  return true;
}

void TestRecipeReplayer::SimulateKeyPressWrapper(
    content::WebContents* web_contents,
    ui::DomKey key) {
  ui::KeyboardCode key_code = ui::NonPrintableDomKeyToKeyboardCode(key);
  ui::DomCode code = ui::UsLayoutKeyboardCodeToDomCode(key_code);
  SimulateKeyPress(web_contents, key, code, key_code, false, false, false,
                   false);
}

bool TestRecipeReplayer::HasChromeStoredCredential(
    const base::DictValue& action,
    bool* stored_cred) {
  std::optional<std::string> origin =
      FindPopulateString(action, "origin", "Origin");
  std::optional<std::string> username =
      FindPopulateString(action, "userName", "Username");
  std::optional<std::string> password =
      FindPopulateString(action, "password", "Password");
  if (!origin || !username || !password)
    return false;
  *stored_cred = feature_action_executor()->HasChromeStoredCredential(
      *origin, *username, *password);

  return true;
}

bool TestRecipeReplayer::SetupSavedAutofillProfile(
    base::ListValue saved_autofill_profile_container) {
  for (auto& list_entry : saved_autofill_profile_container) {
    if (!list_entry.is_dict()) {
      ADD_FAILURE() << "Failed to extract an entry!";
      return false;
    }

    const base::DictValue list_entry_dict = std::move(list_entry).TakeDict();
    std::optional<std::string> type =
        FindPopulateString(list_entry_dict, "type", "profile field type");
    std::optional<std::string> value =
        FindPopulateString(list_entry_dict, "value", "profile field value");

    if (!type || !value)
      return false;

    if (!feature_action_executor()->AddAutofillProfileInfo(*type, *value)) {
      return false;
    }
  }

  // Skip this step if autofill profile is empty.
  // Only Autofill Captured Sites test recipes will have non-empty autofill
  // profiles. Recipes for other captured sites tests will have empty autofill
  // profiles. This block prevents these other tests from failing because
  // the test feature action executor does not know how to setup the autofill
  // profile.
  if (saved_autofill_profile_container.empty()) {
    return true;
  }

  return feature_action_executor()->SetupAutofillProfile();
}

bool TestRecipeReplayer::SetupSavedPasswords(
    base::ListValue saved_password_list_container) {
  for (auto& entry : saved_password_list_container) {
    if (!entry.is_dict()) {
      ADD_FAILURE() << "Failed to extract a saved password!";
      return false;
    }

    const base::DictValue entry_dict = std::move(entry.GetDict());

    std::optional<std::string> origin =
        FindPopulateString(entry_dict, "website", "Website");
    std::optional<std::string> username =
        FindPopulateString(entry_dict, "username", "Username");
    std::optional<std::string> password =
        FindPopulateString(entry_dict, "password", "Password");
    if (!origin || !username || !password)
      return false;

    if (!feature_action_executor()->AddCredential(*origin, *username,
          *password)) {
      return false;
    }
  }

  return true;
}

// TestRecipeReplayChromeFeatureActionExecutor --------------------------------
TestRecipeReplayChromeFeatureActionExecutor::
    TestRecipeReplayChromeFeatureActionExecutor() = default;
TestRecipeReplayChromeFeatureActionExecutor::
    ~TestRecipeReplayChromeFeatureActionExecutor() = default;

bool TestRecipeReplayChromeFeatureActionExecutor::AutofillForm(
    const std::string& focus_element_css_selector,
    const std::vector<std::string>& iframe_path,
    const int attempts,
    content::RenderFrameHost* frame,
    std::optional<autofill::FieldType> triggered_field_type) {
  ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor::AutofillForm "
                   "is not implemented!";
  return false;
}

bool TestRecipeReplayChromeFeatureActionExecutor::AddAutofillProfileInfo(
    const std::string& field_type,
    const std::string& field_value) {
  ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
                   "::AddAutofillProfileInfo is not implemented!";
  return false;
}

bool TestRecipeReplayChromeFeatureActionExecutor::SetupAutofillProfile() {
  ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
                   "::SetupAutofillProfile is not implemented!";
  return false;
}

bool TestRecipeReplayChromeFeatureActionExecutor::AddCredential(
    const std::string& origin,
    const std::string& username,
    const std::string& password) {
  ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor::AddCredential"
                   " is not implemented!";
  return false;
}

bool TestRecipeReplayChromeFeatureActionExecutor::SavePassword() {
  ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor::SavePassword"
                   " is not implemented!";
  return false;
}

bool TestRecipeReplayChromeFeatureActionExecutor::UpdatePassword() {
  ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
                   "::UpdatePassword is not implemented!";
  return false;
}

bool TestRecipeReplayChromeFeatureActionExecutor::WaitForSaveFallback() {
  ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
                   "::WaitForSaveFallback is not implemented!";
  return false;
}

bool TestRecipeReplayChromeFeatureActionExecutor::
    IsChromeShowingPasswordGenerationPrompt() {
  ADD_FAILURE()
      << "TestRecipeReplayChromeFeatureActionExecutor"
         "::IsChromeShowingPasswordGenerationPrompt is not implemented!";
  return false;
}

bool TestRecipeReplayChromeFeatureActionExecutor::
    HasChromeShownSavePasswordPrompt() {
  ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
                   "::HasChromeShownSavePasswordPrompt is not implemented!";
  return false;
}

bool TestRecipeReplayChromeFeatureActionExecutor::TriggerPasswordChange(
    const GURL& url) {
  ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
                   "::TriggerPasswordChange is not implemented!";
  return false;
}

bool TestRecipeReplayChromeFeatureActionExecutor::WaitForPasswordChangeState(
    int state) {
  ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
                   "::WaitForPasswordChangeState is not implemented!";
  return false;
}

bool TestRecipeReplayChromeFeatureActionExecutor::HasChromeStoredCredential(
    const std::string& origin,
    const std::string& username,
    const std::string& password) {
  ADD_FAILURE() << "TestRecipeReplayChromeFeatureActionExecutor"
                   "::HasChromeStoredCredential is not implemented!";
  return false;
}

}  // namespace captured_sites_test_utils
