// 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.

#ifndef CHROME_BROWSER_AUTOFILL_CAPTURED_SITES_TEST_UTILS_H_
#define CHROME_BROWSER_AUTOFILL_CAPTURED_SITES_TEST_UTILS_H_

#include <fstream>
#include <memory>
#include <optional>
#include <string>
#include <vector>

#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/functional/callback_forward.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/strcat.h"
#include "base/test/test_future.h"
#include "base/time/time_override.h"
#include "base/types/strong_alias.h"
#include "base/values.h"
#include "chrome/browser/ui/browser.h"
#include "components/autofill/core/browser/data_model/addresses/autofill_profile.h"
#include "components/autofill/core/browser/data_model/payments/credit_card.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/global_routing_id.h"
#include "content/public/test/browser_test_utils.h"
#include "services/network/public/cpp/network_switches.h"
#include "url/gurl.h"
#include "url/scheme_host_port.h"

namespace content {
class RenderFrameHost;
class WebContents;
}  // namespace content

namespace captured_sites_test_utils {

// The amount of time to wait for an action to complete, or for a page element
// to appear. The Captured Site Automation Framework uses this timeout to break
// out of wait loops in the event that
// 1. A page load error occurred and the page does not have a page element
//    the test expects. Test should stop waiting.
// 2. A page contains persistent animation (such as a flash sale count down
//    timer) that causes the RenderFrameHost count to never diminish completely.
//    Test should stop waiting if a sufficiently large time has expired for the
//    page to load or for the page to respond to the last user action.
const base::TimeDelta default_action_timeout = base::Seconds(30);
// The amount of time to wait for a page to trigger a paint in response to a
// an action. The Captured Site Automation Framework uses this timeout to
// break out of a wait loop after a hover action.
const base::TimeDelta visual_update_timeout = base::Seconds(20);
// The amount of time to do a precheck on the page before going to a click
// fallback action.
const base::TimeDelta click_fallback_timeout = base::Seconds(5);
// When we cause a scroll event, make sure we give the page a moment to react
// before continuing.
const base::TimeDelta scroll_wait_timeout = base::Seconds(2);
// Some times, tests tend to need a break that can't be read from the elements
// play status.
const base::TimeDelta cool_off_action_timeout = base::Seconds(1);
// The time to wait between checks for a page to become idle or active based on
// the loading status and then the render frame count.
const base::TimeDelta wait_for_idle_loop_length = base::Milliseconds(500);

std::string FilePathToUTF8(const base::FilePath::StringType& str);

enum ExpectedResult { kPass, kFail };
struct CapturedSiteParams {
  std::string scenario_dir;
  std::string site_name;
  std::optional<int> bug_number;
  ExpectedResult expectation = kPass;
  bool is_disabled = false;
  base::FilePath capture_file_path;
  base::FilePath recipe_file_path;
  base::FilePath refresh_file_path;

  CapturedSiteParams();
  ~CapturedSiteParams();
  CapturedSiteParams(const CapturedSiteParams& other);
};

std::ostream& operator<<(std::ostream& os, const CapturedSiteParams& data);

std::vector<CapturedSiteParams> GetCapturedSites(
    const base::FilePath& replay_files_dir_path);

struct GetParamAsString {
  template <class ParamType>
  std::string operator()(const testing::TestParamInfo<ParamType>& info) const {
    if (info.param.scenario_dir.empty())
      return info.param.site_name;
    return base::StrCat({info.param.scenario_dir, "_", info.param.site_name});
  }
};

// Reads the recipe file and returns the recipe as a base::DictValue.
std::optional<base::DictValue> ReadRecipeFile(
    const base::FilePath& recipe_file_path);

std::optional<base::FilePath> GetCommandFilePath();

// Prints tips on how to run captured-site tests.
// `test_file_name` should be without the .cc suffix.
void PrintInstructions(const char* test_file_name);

[[nodiscard]] content::RenderFrameHost* WaitForFrameMatchingName(
    content::WebContents& web_contents,
    const std::string& name,
    const base::TimeDelta timeout = default_action_timeout);
[[nodiscard]] content::RenderFrameHost* WaitForFrameMatchingOrigin(
    content::WebContents& web_contents,
    const url::SchemeHostPort& origin,
    const base::TimeDelta timeout = default_action_timeout);
[[nodiscard]] content::RenderFrameHost* WaitForFrameMatchingUrl(
    content::WebContents& web_contents,
    const GURL& url,
    const base::TimeDelta timeout = default_action_timeout);
[[nodiscard]] content::RenderFrameHost* WaitForFrame(
    content::WebContents& web_contents,
    base::RepeatingCallback<bool(content::RenderFrameHost*)> predicate,
    const base::TimeDelta timeout = default_action_timeout);

// WebPageReplayServerWrapper

// WebPageReplayServerWrapper is a helper wrapper that controls the configuring
// and running the WebPageReplay Server instance.
// TODO(b/399665693): Consider moving this to a shared WPR utilities code
// location.
class WebPageReplayServerWrapper {
 public:
  explicit WebPageReplayServerWrapper(
      bool start_as_replay,
      int host_http_port = 8080,
      int host_https_port = 8081,
      // Passes additional arguments used in the WPR command.
      std::vector<std::string> extra_args = {});

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

  ~WebPageReplayServerWrapper();

  bool Start(const base::FilePath& capture_file_path);
  bool Stop();

 private:
  bool RunWebPageReplayCmdAndWaitForExit(
      const std::vector<std::string>& args,
      const base::TimeDelta& timeout = base::Seconds(5));
  bool RunWebPageReplayCmd(const std::vector<std::string>& args);

  std::string cmd_name() { return start_as_replay_ ? "replay" : "record"; }
  // The Web Page Replay server that serves the captured sites.
  base::Process web_page_replay_server_;

  int host_http_port_;
  int host_https_port_;
  bool start_as_replay_;
  std::vector<std::string> extra_args_;
};

class ProfileDataController {
 public:
  ProfileDataController();
  ~ProfileDataController();

  const autofill::CreditCard& credit_card() const { return card_; }
  const autofill::AutofillProfile& profile() { return profile_; }
  bool AddAutofillProfileInfo(const std::string& field_type,
                              const std::string& field_value);
  std::optional<std::u16string> cvc() const { return cvc_; }

 private:
  // If a CVC is available in the Action Recorder receipt, this test uses a
  // server card to autofill the payment form. So the "Enter CVC" dialog will
  // pop up for card autofill. Otherwise, this test uses a local card to
  // autofill the payment form.
  std::optional<std::u16string> cvc_;
  autofill::AutofillProfile profile_;
  autofill::CreditCard card_;
};

// TestRecipeReplayChromeFeatureActionExecutor
//
// TestRecipeReplayChromeFeatureActionExecutor is a helper interface. A
// TestRecipeReplayChromeFeatureActionExecutor class implements functions
// that automate Chrome feature behavior. TestRecipeReplayer calls
// TestRecipeReplayChromeFeatureActionExecutor functions to execute actions
// that involves a Chrome feature - such as Chrome Autofill or Chrome
// Password Manager. Executing a Chrome feature action typically require
// using private or protected hooks defined inside that feature's
// InProcessBrowserTest class. By implementing this interface an
// InProcessBrowserTest exposes its feature to captured site automation.
class TestRecipeReplayChromeFeatureActionExecutor {
 public:
  TestRecipeReplayChromeFeatureActionExecutor(
      const TestRecipeReplayChromeFeatureActionExecutor&) = delete;
  TestRecipeReplayChromeFeatureActionExecutor& operator=(
      const TestRecipeReplayChromeFeatureActionExecutor&) = delete;

  // Chrome Autofill feature methods.
  // Triggers Chrome Autofill in the specified input element on the specified
  // document.
  virtual bool 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);
  virtual bool AddAutofillProfileInfo(const std::string& field_type,
                                      const std::string& field_value);
  virtual bool SetupAutofillProfile();
  // Chrome Password Manager feature methods.
  virtual bool AddCredential(const std::string& origin,
                             const std::string& username,
                             const std::string& password);
  virtual bool SavePassword();
  virtual bool UpdatePassword();
  virtual bool WaitForSaveFallback();
  virtual bool IsChromeShowingPasswordGenerationPrompt();
  virtual bool HasChromeShownSavePasswordPrompt();
  virtual bool TriggerPasswordChange(const GURL& url);
  // Waits for specified state of Password Change. Possible correspond to
  // PasswordChangeDelegate::State.
  virtual bool WaitForPasswordChangeState(int state);
  virtual bool HasChromeStoredCredential(const std::string& origin,
                                         const std::string& username,
                                         const std::string& password);

 protected:
  TestRecipeReplayChromeFeatureActionExecutor();
  ~TestRecipeReplayChromeFeatureActionExecutor();
};

// TestRecipeReplayer
//
// The TestRecipeReplayer object drives Captured Site Automation by
// 1. Providing a set of functions that help an InProcessBrowserTest to
//    configure, start and stop a Web Page Replay (WPR) server. A WPR server
//    is a local server that intercepts and responds to Chrome requests with
//    pre-recorded traffic. Using a captured site archive file, WPR can
//    mimick the site server and provide the test with deterministic site
//    behaviors.
// 2. Providing a function that deserializes and replays a Test Recipe. A Test
//    Recipe is a JSON formatted file containing instructions on how to run a
//    Chrome test against a live or captured site. These instructions include
//    the starting URL for the test, and a list of user actions (clicking,
//    typing) that drives the test. One may sample some example Test Recipes
//    under the src/chrome/test/data/autofill/captured_sites directory.
class TestRecipeReplayer {
 public:
  static constexpr int kHostHttpPort = 8080;
  static constexpr int kHostHttpsPort = 8081;
  static constexpr int kHostHttpRecordPort = 8082;
  static constexpr int kHostHttpsRecordPort = 8083;

  enum DomElementReadyState {
    kReadyStatePresent = 0,
    kReadyStateVisible = 1 << 0,
    kReadyStateEnabled = 1 << 1,
    kReadyStateOnTop   = 1 << 2
  };

  TestRecipeReplayer(
      Browser* browser,
      TestRecipeReplayChromeFeatureActionExecutor* feature_action_executor);

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

  ~TestRecipeReplayer();

  // Replay a test by:
  // 1. Starting a WPR server using the specified capture file.
  // 2. Replaying the specified Test Recipe file.
  bool ReplayTest(const base::FilePath& capture_file_path,
                  const base::FilePath& recipe_file_path,
                  const std::optional<base::FilePath>& command_file_path);

  const std::vector<testing::AssertionResult> GetValidationFailures() const;

  static void SetUpCommandLine(base::CommandLine* command_line);
  static void SetUpHostResolverRules(base::CommandLine* command_line);
  static bool ScrollElementIntoView(const std::string& element_xpath,
                                    content::RenderFrameHost* frame);
  static bool PlaceFocusOnElement(const std::string& element_xpath,
                                  const std::vector<std::string>& iframe_path,
                                  content::RenderFrameHost* frame);
  static bool GetBoundingRectOfTargetElement(
      const std::string& target_element_xpath,
      const std::vector<std::string>& iframe_path,
      content::RenderFrameHost* frame,
      gfx::Rect* output_rect);
  static bool SimulateLeftMouseClickAt(
      const gfx::Point& point,
      content::RenderFrameHost* render_frame_host);
  static bool SimulateMouseHoverAt(content::RenderFrameHost* render_frame_host,
                                   const gfx::Point& point);

 private:
  using IgnoreCase = base::StrongAlias<struct IgnoreCaseTag, bool>;

  static bool GetIFrameOffsetFromIFramePath(
      const std::vector<std::string>& iframe_path,
      content::RenderFrameHost* frame,
      gfx::Vector2d* offset);
  static bool GetBoundingRectOfTargetElement(
      const std::string& target_element_xpath,
      content::RenderFrameHost* frame,
      gfx::Rect* output_rect);

  Browser* browser();

  TestRecipeReplayChromeFeatureActionExecutor* feature_action_executor();
  WebPageReplayServerWrapper* web_page_replay_server_wrapper();
  content::WebContents* GetWebContents();
  void CleanupSiteData();
  bool StartWebPageReplayServer(base::Process* web_page_replay_server,
                                const base::FilePath& capture_file_path,
                                const bool start_as_replay = true);
  bool StopWebPageReplayServer(base::Process* web_page_replay_server);
  bool ReplayRecordedActions(
      const base::FilePath& recipe_file_path,
      const std::optional<base::FilePath>& command_file_path);
  bool InitializeBrowserToExecuteRecipe(base::DictValue& recipe);
  bool ExecuteAutofillAction(base::DictValue action);
  bool ExecuteClickAction(base::DictValue action);
  bool ExecuteClickIfNotSeenAction(base::DictValue action);
  bool ExecuteCoolOffAction(base::DictValue action);
  bool ExecuteCloseTabAction(base::DictValue action);
  bool ExecuteHoverAction(base::DictValue action);
  bool ExecuteForceLoadPage(base::DictValue action);
  bool ExecutePressEnterAction(base::DictValue action);
  bool ExecutePressEscapeAction(base::DictValue action);
  bool ExecutePressSpaceAction(base::DictValue action);
  bool ExecuteRunCommandAction(base::DictValue action);
  bool ExecuteSavePasswordAction(base::DictValue action);
  bool ExecuteSelectDropdownAction(base::DictValue action);
  bool ExecuteTypeAction(base::DictValue action);
  bool ExecuteTypePasswordAction(base::DictValue action);
  bool ExecuteUpdatePasswordAction(base::DictValue action);
  bool ExecuteValidateFieldValueAction(base::DictValue action);
  bool ExecuteValidateNoSavePasswordPromptAction(base::DictValue action);
  bool ExecuteValidatePasswordGenerationPromptAction(base::DictValue action);
  bool ExecuteTriggerPasswordChangeAction(base::DictValue action);
  bool ExecuteWaitForPasswordChangeStateAction(base::DictValue action);
  bool ExecuteValidateSaveFallbackAction(base::DictValue action);
  bool ExecuteWaitForStateAction(base::DictValue action);
  bool GetTargetHTMLElementXpathFromAction(const base::DictValue& action,
                                           std::string* xpath);
  bool GetTargetFrameFromAction(const base::DictValue& action,
                                content::RenderFrameHost** frame);
  bool GetIFramePathFromAction(const base::DictValue& action,
                               std::vector<std::string>* iframe_path);
  bool GetTargetHTMLElementVisibilityEnumFromAction(
      const base::DictValue& action,
      int* visibility_enum_val);
  bool ExtractFrameAndVerifyElement(const base::DictValue& action,
                                    std::string* xpath,
                                    content::RenderFrameHost** frame,
                                    bool set_focus = false,
                                    bool relaxed_visibility = false,
                                    bool ignore_failure = false);
  void ValidatePasswordGenerationPromptState(
      const content::ToRenderFrameHost& frame,
      const std::string& element_xpath,
      bool expect_to_be_shown);
  // When returning true, `frame` points to the RenderFrameHost of the frame
  // specified in `action`. This is determined dynamically because
  // WaitForElementToBeReady may need to wait for navigations to finish and the
  // frame to be loaded.
  bool WaitForElementToBeReady(const std::string& xpath,
                               const int visibility_enum_val,
                               const base::DictValue& action,
                               content::RenderFrameHost** frame,
                               bool ignore_failure = false);
  bool WaitForStateChange(
      const base::DictValue& action,
      content::RenderFrameHost** frame,
      const std::vector<std::string>& state_assertions,
      const base::TimeDelta& timeout = default_action_timeout,
      bool ignore_failure = false);
  bool AllAssertionsPassed(const content::ToRenderFrameHost& frame,
                           const std::vector<std::string>& assertions);
  bool ExecuteJavaScriptOnElementByXpath(
      const content::ToRenderFrameHost& frame,
      const std::string& element_xpath,
      const std::string& execute_function_body,
      const base::TimeDelta& time_to_wait_for_element = default_action_timeout);
  bool GetElementProperty(const content::ToRenderFrameHost& frame,
                          const std::string& element_xpath,
                          const std::string& get_property_function_body,
                          std::string* property);
  bool ExpectElementPropertyEqualsAnyOf(
      const content::ToRenderFrameHost& frame,
      const std::string& element_xpath,
      const std::string& get_property_function_body,
      const std::vector<std::string>& expected_value,
      const std::string& validation_field,
      IgnoreCase ignore_case = IgnoreCase(false));
  void SimulateKeyPressWrapper(content::WebContents* web_contents,
                               ui::DomKey key);
  bool HasChromeStoredCredential(const base::DictValue& action,
                                 bool* stored_cred);
  bool OverrideTimeClock(const base::FilePath capture_file_path);
  bool SetupSavedAutofillProfile(
      base::ListValue saved_autofill_profile_container);
  bool SetupSavedPasswords(base::ListValue saved_password_list_container);

  // Wait until Chrome finishes loading a page and updating the page's visuals.
  // If Chrome finishes loading a page but continues to paint every half
  // second, exit after `continuous_paint_timeout` expires since Chrome
  // finished loading the page.
  // After calling `WaitTillPageIsIdle()` all RenderFrameHost pointers should
  // be considered potentially invalid because a navigation may have happened.
  void WaitTillPageIsIdle(
      base::TimeDelta continuous_paint_timeout = default_action_timeout);
  // Wait until Chrome makes at least 1 visual update, or until timeout
  // expires. Returns false if no visual update is observed before the given
  // timeout elapses.
  bool WaitForVisualUpdate(base::TimeDelta timeout = visual_update_timeout);

  raw_ptr<Browser> browser_;
  raw_ptr<TestRecipeReplayChromeFeatureActionExecutor> feature_action_executor_;
  // The Web Page Replay server that serves the captured sites.
  std::unique_ptr<captured_sites_test_utils::WebPageReplayServerWrapper>
      web_page_replay_server_wrapper_ =
          std::make_unique<WebPageReplayServerWrapper>(true);

  std::vector<testing::AssertionResult> validation_failures_;

  // Overrides the TimeClock to use the recorded date.
  std::unique_ptr<base::subtle::ScopedTimeClockOverrides> time_override_;
};

}  // namespace captured_sites_test_utils

#endif  // CHROME_BROWSER_AUTOFILL_CAPTURED_SITES_TEST_UTILS_H_
