// 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 "headless/test/headless_protocol_browsertest.h"

#include "base/base64.h"
#include "base/base_paths.h"
#include "base/command_line.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/json/json_reader.h"
#include "base/memory/scoped_refptr.h"
#include "base/path_service.h"
#include "build/build_config.h"
#include "content/public/common/content_switches.h"
#include "headless/lib/browser/headless_web_contents_impl.h"
#include "headless/public/switches.h"
#include "headless/test/headless_browser_test_utils.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "services/network/public/cpp/network_switches.h"

namespace headless {

namespace switches {
static const char kResetResults[] = "reset-results";
static const char kDumpConsoleMessages[] = "dump-console-messages";
static const char kDumpDevToolsProtocol[] = "dump-devtools-protocol";
static const char kDumpTestResult[] = "dump-test-result";
}  // namespace switches

namespace {

static const base::FilePath kTestsDirectory(
    FILE_PATH_LITERAL("headless/test/data/protocol"));

}  // namespace

HeadlessProtocolBrowserTest::HeadlessProtocolBrowserTest() {
  embedded_test_server()->ServeFilesFromSourceDirectory(
      "third_party/blink/web_tests/http/tests/inspector-protocol");
  EXPECT_TRUE(embedded_test_server()->Start());
}

HeadlessProtocolBrowserTest::~HeadlessProtocolBrowserTest() = default;

void HeadlessProtocolBrowserTest::SetUpCommandLine(
    base::CommandLine* command_line) {
  command_line->AppendSwitchASCII(::network::switches::kHostResolverRules,
                                  "MAP *.test 127.0.0.1");
  HeadlessDevTooledBrowserTest::SetUpCommandLine(command_line);

  if (RequiresSitePerProcess()) {
    // Make sure the navigations spawn new processes. We run test harness
    // in one process (harness.test) and tests in another.
    command_line->AppendSwitch(::switches::kSitePerProcess);
  }
  // Make sure proxy related tests are not affected by a platform specific
  // system proxy configuration service.
  command_line->AppendSwitch(switches::kNoSystemProxyConfigService);
}

bool HeadlessProtocolBrowserTest::RequiresSitePerProcess() {
  return true;
}

base::Value::Dict HeadlessProtocolBrowserTest::GetPageUrlExtraParams() {
  return base::Value::Dict();
}

void HeadlessProtocolBrowserTest::RunDevTooledTest() {
  scoped_refptr<content::DevToolsAgentHost> agent_host =
      content::DevToolsAgentHost::GetOrCreateFor(
          HeadlessWebContentsImpl::From(web_contents_)->web_contents());

  // Set up Page domain.
  devtools_client_.AddEventHandler(
      "Page.loadEventFired",
      base::BindRepeating(&HeadlessProtocolBrowserTest::OnLoadEventFired,
                          base::Unretained(this)));
  devtools_client_.SendCommand("Page.enable");

  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kDumpConsoleMessages)) {
    // Set up Runtime domain to intercept console messages.
    devtools_client_.AddEventHandler(
        "Runtime.consoleAPICalled",
        base::BindRepeating(&HeadlessProtocolBrowserTest::OnConsoleAPICalled,
                            base::Unretained(this)));
    devtools_client_.SendCommand("Runtime.enable");
  }

  // Expose DevTools protocol to the target.
  browser_devtools_client_.SendCommand("Target.exposeDevToolsProtocol",
                                       Param("targetId", agent_host->GetId()));

  // Navigate to test harness page
  GURL page_url = embedded_test_server()->GetURL(
      "harness.test", "/protocol/inspector-protocol-test.html");
  devtools_client_.SendCommand("Page.navigate", Param("url", page_url.spec()));
}

void HeadlessProtocolBrowserTest::OnLoadEventFired(
    const base::Value::Dict& params) {
  ASSERT_THAT(params, DictHasValue("method", "Page.loadEventFired"));

  base::ScopedAllowBlockingForTesting allow_blocking;
  base::FilePath src_dir;
  CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir));
  base::FilePath test_path =
      src_dir.Append(kTestsDirectory).AppendASCII(script_name_);
  std::string script;
  if (!base::ReadFileToString(test_path, &script)) {
    ADD_FAILURE() << "Unable to read test at " << test_path;
    FinishTest();
    return;
  }
  GURL test_url = embedded_test_server()->GetURL("harness.test",
                                                 "/protocol/" + script_name_);
  GURL target_url =
      embedded_test_server()->GetURL("127.0.0.1", "/protocol/" + script_name_);

  base::Value::Dict test_params;
  test_params.Set("test", test_url.spec());
  test_params.Set("target", target_url.spec());
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kDumpDevToolsProtocol)) {
    test_params.Set("dumpDevToolsProtocol", true);
  }
  test_params.Merge(GetPageUrlExtraParams());

  std::string json_test_params;
  base::JSONWriter::Write(test_params, &json_test_params);
  std::string evaluate_script = "runTest(" + json_test_params + ")";

  base::Value::Dict evaluate_params;
  evaluate_params.Set("expression", evaluate_script);
  evaluate_params.Set("awaitPromise", true);
  evaluate_params.Set("returnByValue", true);
  devtools_client_.SendCommand(
      "Runtime.evaluate", std::move(evaluate_params),
      base::BindOnce(&HeadlessProtocolBrowserTest::OnEvaluateResult,
                     base::Unretained(this)));
}

void HeadlessProtocolBrowserTest::OnEvaluateResult(base::Value::Dict params) {
  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kDumpTestResult)) {
    LOG(ERROR) << "Test result:\n" << params.DebugString();
  }

  ProcessTestResult(DictString(params, "result.result.value"));

  FinishTest();
}

void HeadlessProtocolBrowserTest::ProcessTestResult(
    const std::string& test_result) {
  base::ScopedAllowBlockingForTesting allow_blocking;

  base::FilePath src_dir;
  CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir));
  base::FilePath expectation_path =
      src_dir.Append(kTestsDirectory)
          .AppendASCII(script_name_.substr(0, script_name_.length() - 3) +
                       "-expected.txt");

  if (base::CommandLine::ForCurrentProcess()->HasSwitch(
          switches::kResetResults)) {
    LOG(INFO) << "Updating expectations at " << expectation_path;
    bool succcess = base::WriteFile(expectation_path, test_result);
    CHECK(succcess);
  }

  std::string expectation;
  if (!base::ReadFileToString(expectation_path, &expectation)) {
    ADD_FAILURE() << "Unable to read expectations at " << expectation_path;
  }

  EXPECT_EQ(expectation, test_result);
}

void HeadlessProtocolBrowserTest::OnConsoleAPICalled(
    const base::Value::Dict& params) {
  ASSERT_THAT(params, DictHasValue("method", "Runtime.consoleAPICalled"));

  const base::Value::List* args = params.FindListByDottedPath("params.args");
  if (!args || args->empty())
    return;

  const base::Value* value = args->front().GetDict().Find("value");
  switch (value->type()) {
    case base::Value::Type::NONE:
    case base::Value::Type::BOOLEAN:
    case base::Value::Type::INTEGER:
    case base::Value::Type::DOUBLE:
    case base::Value::Type::STRING:
      LOG(INFO) << value->DebugString();
      return;
    default:
      LOG(INFO) << "Unhandled value type: " << value->type();
      return;
  }
}

void HeadlessProtocolBrowserTest::FinishTest() {
  test_finished_ = true;
  FinishAsynchronousTest();
}

// TODO(crbug.com/1086872): The whole test suite is flaky on Mac ASAN.
#if (BUILDFLAG(IS_MAC) && defined(ADDRESS_SANITIZER))
#define HEADLESS_PROTOCOL_TEST(TEST_NAME, SCRIPT_NAME)                        \
  IN_PROC_BROWSER_TEST_F(HeadlessProtocolBrowserTest, DISABLED_##TEST_NAME) { \
    test_folder_ = "/protocol/";                                              \
    script_name_ = SCRIPT_NAME;                                               \
    RunTest();                                                                \
  }
#else
#define HEADLESS_PROTOCOL_TEST(TEST_NAME, SCRIPT_NAME)             \
  IN_PROC_BROWSER_TEST_F(HeadlessProtocolBrowserTest, TEST_NAME) { \
    test_folder_ = "/protocol/";                                   \
    script_name_ = SCRIPT_NAME;                                    \
    RunTest();                                                     \
  }
#endif

// Headless-specific tests
HEADLESS_PROTOCOL_TEST(VirtualTimeBasics, "emulation/virtual-time-basics.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeInterrupt,
                       "emulation/virtual-time-interrupt.js")

// Flaky on Linux, Mac & Win. TODO(crbug.com/930717): Re-enable.
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_MAC) || \
    BUILDFLAG(IS_WIN) || BUILDFLAG(IS_FUCHSIA)
#define MAYBE_VirtualTimeCrossProcessNavigation \
  DISABLED_VirtualTimeCrossProcessNavigation
#else
#define MAYBE_VirtualTimeCrossProcessNavigation \
  VirtualTimeCrossProcessNavigation
#endif
HEADLESS_PROTOCOL_TEST(MAYBE_VirtualTimeCrossProcessNavigation,
                       "emulation/virtual-time-cross-process-navigation.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeDetachFrame,
                       "emulation/virtual-time-detach-frame.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeNoBlock404, "emulation/virtual-time-404.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeLocalStorage,
                       "emulation/virtual-time-local-storage.js")
HEADLESS_PROTOCOL_TEST(VirtualTimePendingScript,
                       "emulation/virtual-time-pending-script.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeRedirect,
                       "emulation/virtual-time-redirect.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeSessionStorage,
                       "emulation/virtual-time-session-storage.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeStarvation,
                       "emulation/virtual-time-starvation.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeVideo, "emulation/virtual-time-video.js")
// Flaky on all platforms. https://crbug.com/1295644
HEADLESS_PROTOCOL_TEST(DISABLED_VirtualTimeErrorLoop,
                       "emulation/virtual-time-error-loop.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeFetchStream,
                       "emulation/virtual-time-fetch-stream.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeFetchReadBody,
                       "emulation/virtual-time-fetch-read-body.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeDialogWhileLoading,
                       "emulation/virtual-time-dialog-while-loading.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeHistoryNavigation,
                       "emulation/virtual-time-history-navigation.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeHistoryNavigationSameDoc,
                       "emulation/virtual-time-history-navigation-same-doc.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeSVG, "emulation/virtual-time-svg.js")

// Flaky on Mac. TODO(crbug.com/1419801): Re-enable.
#if BUILDFLAG(IS_MAC)
#define MAYBE_VirtualTimeWorkerBasic DISABLED_VirtualTimeWorkerBasic
#else
#define MAYBE_VirtualTimeWorkerBasic VirtualTimeWorkerBasic
#endif
HEADLESS_PROTOCOL_TEST(MAYBE_VirtualTimeWorkerBasic,
                       "emulation/virtual-time-worker-basic.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeWorkerLockstep,
                       "emulation/virtual-time-worker-lockstep.js")

// Flaky on Mac. TODO(crbug.com/1419801): Re-enable.
#if BUILDFLAG(IS_MAC)
#define MAYBE_VirtualTimeWorkerFetch DISABLED_VirtualTimeWorkerFetch
#else
#define MAYBE_VirtualTimeWorkerFetch VirtualTimeWorkerFetch
#endif
HEADLESS_PROTOCOL_TEST(MAYBE_VirtualTimeWorkerFetch,
                       "emulation/virtual-time-worker-fetch.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeWorkerTerminate,
                       "emulation/virtual-time-worker-terminate.js")

// Flaky on Mac. TODO(crbug.com/1164173): Re-enable.
#if BUILDFLAG(IS_MAC)
#define MAYBE_VirtualTimeFetchKeepalive DISABLED_VirtualTimeFetchKeepalive
#else
#define MAYBE_VirtualTimeFetchKeepalive VirtualTimeFetchKeepalive
#endif
HEADLESS_PROTOCOL_TEST(MAYBE_VirtualTimeFetchKeepalive,
                       "emulation/virtual-time-fetch-keepalive.js")
HEADLESS_PROTOCOL_TEST(VirtualTimeDisposeWhileRunning,
                       "emulation/virtual-time-dispose-while-running.js")
HEADLESS_PROTOCOL_TEST(VirtualTimePausesDocumentLoading,
                       "emulation/virtual-time-pauses-document-loading.js")

HEADLESS_PROTOCOL_TEST(PageBeforeUnload, "page/page-before-unload.js")

// http://crbug.com/633321
#if BUILDFLAG(IS_ANDROID)
#define MAYBE_VirtualTimeTimerOrder DISABLED_VirtualTimeTimerOrder
#define MAYBE_VirtualTimeTimerSuspend DISABLED_VirtualTimeTimerSuspend
#else
#define MAYBE_VirtualTimeTimerOrder VirtualTimeTimerOrder
#define MAYBE_VirtualTimeTimerSuspend VirtualTimeTimerSuspend
#endif
HEADLESS_PROTOCOL_TEST(MAYBE_VirtualTimeTimerOrder,
                       "emulation/virtual-time-timer-order.js")
HEADLESS_PROTOCOL_TEST(MAYBE_VirtualTimeTimerSuspend,
                       "emulation/virtual-time-timer-suspended.js")
#undef MAYBE_VirtualTimeTimerOrder
#undef MAYBE_VirtualTimeTimerSuspend

HEADLESS_PROTOCOL_TEST(Geolocation, "emulation/geolocation-crash.js")

HEADLESS_PROTOCOL_TEST(DragStarted, "input/dragIntercepted.js")

// https://crbug.com/1414190
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
#define MAYBE_InputClipboardOps DISABLED_InputClipboardOps
#else
#define MAYBE_InputClipboardOps InputClipboardOps
#endif
HEADLESS_PROTOCOL_TEST(MAYBE_InputClipboardOps, "input/input-clipboard-ops.js")

HEADLESS_PROTOCOL_TEST(ClipboardApiCopyPaste,
                       "input/clipboard-api-copy-paste.js")

HEADLESS_PROTOCOL_TEST(FocusBlurNotifications,
                       "input/focus-blur-notifications.js")

HEADLESS_PROTOCOL_TEST(HeadlessSessionBasicsTest,
                       "sessions/headless-session-basics.js")

HEADLESS_PROTOCOL_TEST(HeadlessSessionCreateContextDisposeOnDetach,
                       "sessions/headless-createContext-disposeOnDetach.js")

HEADLESS_PROTOCOL_TEST(BrowserSetInitialProxyConfig,
                       "sanity/browser-set-initial-proxy-config.js")

HEADLESS_PROTOCOL_TEST(BrowserUniversalNetworkAccess,
                       "sanity/universal-network-access.js")

HEADLESS_PROTOCOL_TEST(ShowDirectoryPickerNoCrash,
                       "sanity/show-directory-picker-no-crash.js")

HEADLESS_PROTOCOL_TEST(ShowFilePickerInterception,
                       "sanity/show-file-picker-interception.js")

HEADLESS_PROTOCOL_TEST(WindowSizeOnStart, "sanity/window-size-on-start.js")

HEADLESS_PROTOCOL_TEST(LargeBrowserWindowSize,
                       "sanity/large-browser-window-size.js")

HEADLESS_PROTOCOL_TEST(ScreencastBasics, "sanity/screencast-basics.js")

class HeadlessProtocolBrowserTestWithProxy
    : public HeadlessProtocolBrowserTest {
 public:
  HeadlessProtocolBrowserTestWithProxy()
      : proxy_server_(net::EmbeddedTestServer::TYPE_HTTP) {
    proxy_server_.AddDefaultHandlers(
        base::FilePath(FILE_PATH_LITERAL("headless/test/data")));
  }

  void SetUp() override {
    ASSERT_TRUE(proxy_server_.Start());
    HeadlessProtocolBrowserTest::SetUp();
  }

  void TearDown() override {
    EXPECT_TRUE(proxy_server_.ShutdownAndWaitUntilComplete());
    HeadlessProtocolBrowserTest::TearDown();
  }

  net::EmbeddedTestServer* proxy_server() { return &proxy_server_; }

 protected:
  base::Value::Dict GetPageUrlExtraParams() override {
    std::string proxy = proxy_server()->host_port_pair().ToString();
    base::Value::Dict dict;
    dict.Set("proxy", proxy);
    return dict;
  }

 private:
  net::EmbeddedTestServer proxy_server_;
};

#define HEADLESS_PROTOCOL_TEST_WITH_PROXY(TEST_NAME, SCRIPT_NAME)           \
  IN_PROC_BROWSER_TEST_F(HeadlessProtocolBrowserTestWithProxy, TEST_NAME) { \
    test_folder_ = "/protocol/";                                            \
    script_name_ = SCRIPT_NAME;                                             \
    RunTest();                                                              \
  }

HEADLESS_PROTOCOL_TEST_WITH_PROXY(BrowserSetProxyConfig,
                                  "sanity/browser-set-proxy-config.js")

// TODO(crbug.com/1086872): The whole test suite is flaky on Mac ASAN.
#if (BUILDFLAG(IS_MAC) && defined(ADDRESS_SANITIZER))
#define MAYBE_IN_PROC_BROWSER_TEST_F(CLASS, TEST_NAME) \
  IN_PROC_BROWSER_TEST_F(CLASS, DISABLED_##TEST_NAME)
#else
#define MAYBE_IN_PROC_BROWSER_TEST_F(CLASS, TEST_NAME) \
  IN_PROC_BROWSER_TEST_F(CLASS, TEST_NAME)
#endif

#define HEADLESS_PROTOCOL_TEST_WITHOUT_SITE_ISOLATION(TEST_NAME, SCRIPT_NAME) \
  MAYBE_IN_PROC_BROWSER_TEST_F(                                               \
      HeadlessProtocolBrowserTestWithoutSiteIsolation, TEST_NAME) {           \
    test_folder_ = "/protocol/";                                              \
    script_name_ = SCRIPT_NAME;                                               \
    RunTest();                                                                \
  }

class HeadlessProtocolBrowserTestWithoutSiteIsolation
    : public HeadlessProtocolBrowserTest {
 public:
  HeadlessProtocolBrowserTestWithoutSiteIsolation() = default;

 protected:
  bool RequiresSitePerProcess() override { return false; }
};

HEADLESS_PROTOCOL_TEST_WITHOUT_SITE_ISOLATION(
    VirtualTimeLocalStorageDetachedFrame,
    "emulation/virtual-time-local-storage-detached-frame.js")

class HeadlessProtocolBrowserTestWithDataPath
    : public HeadlessProtocolBrowserTest {
 protected:
  base::Value::Dict GetPageUrlExtraParams() override {
    base::FilePath src_dir;
    CHECK(base::PathService::Get(base::DIR_SOURCE_ROOT, &src_dir));
    base::FilePath path =
        src_dir.Append(kTestsDirectory).AppendASCII(data_path_);
    base::Value::Dict dict;
    dict.Set("data_path", path.AsUTF8Unsafe());
    return dict;
  }

  std::string data_path_;
};

#define HEADLESS_PROTOCOL_TEST_WITH_DATA_PATH(TEST_NAME, SCRIPT_NAME, PATH) \
  MAYBE_IN_PROC_BROWSER_TEST_F(HeadlessProtocolBrowserTestWithDataPath,     \
                               TEST_NAME) {                                 \
    test_folder_ = "/protocol/";                                            \
    script_name_ = SCRIPT_NAME;                                             \
    data_path_ = PATH;                                                      \
    RunTest();                                                              \
  }

// TODO(crbug.com/1399463)  Re-enable after resolving flaky failures.
HEADLESS_PROTOCOL_TEST_WITH_DATA_PATH(
    FileInputDirectoryUpload,
    "sanity/file-input-directory-upload.js",
    "sanity/resources/file-input-directory-upload")

}  // namespace headless