// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <memory>

#include "base/base_paths.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/json/json_reader.h"
#include "base/json/json_writer.h"
#include "base/path_service.h"
#include "base/strings/string_util.h"
#include "base/threading/thread_restrictions.h"
#include "base/values.h"
#include "build/build_config.h"
#include "components/devtools/simple_devtools_protocol_client/simple_devtools_protocol_client.h"
#include "content/public/browser/render_widget_host_view.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/url_constants.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/no_renderer_crashes_assertion.h"
#include "headless/lib/browser/headless_web_contents_impl.h"
#include "headless/public/switches.h"
#include "headless/test/headless_browser_test.h"
#include "headless/test/headless_browser_test_utils.h"
#include "headless/test/headless_devtooled_browsertest.h"
#include "net/test/spawned_test_server/spawned_test_server.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/chrome_debug_urls.h"
#include "third_party/inspector_protocol/crdtp/dispatch.h"
#include "url/gurl.h"

#define EXPECT_SIZE_EQ(expected, actual)               \
  do {                                                 \
    EXPECT_EQ((expected).width(), (actual).width());   \
    EXPECT_EQ((expected).height(), (actual).height()); \
  } while (false)

using simple_devtools_protocol_client::SimpleDevToolsProtocolClient;

using testing::ElementsAre;
using testing::NotNull;
using testing::UnorderedElementsAre;

namespace headless {

class HeadlessDevToolsClientNavigationTest
    : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Page.loadEventFired",
        base::BindRepeating(
            &HeadlessDevToolsClientNavigationTest::OnLoadEventFired,
            base::Unretained(this)));
    SendCommandSync(devtools_client_, "Page.enable");

    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url", embedded_test_server()->GetURL("/hello.html").spec()));
  }

  void OnLoadEventFired(const base::Value::Dict& params) {
    devtools_client_.SendCommand("Page.disable");
    FinishAsynchronousTest();
  }
};

HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsClientNavigationTest);

class HeadlessDevToolsClientWindowManagementTest
    : public HeadlessDevTooledBrowserTest {
 public:
  int window_id() {
    return HeadlessWebContentsImpl::From(web_contents_)->window_id();
  }

  void SetWindowBounds(
      const gfx::Rect& rect,
      SimpleDevToolsProtocolClient::ResponseCallback callback) {
    base::Value::Dict params;
    params.Set("windowId", window_id());
    params.SetByDottedPath("bounds.left", rect.x());
    params.SetByDottedPath("bounds.top", rect.y());
    params.SetByDottedPath("bounds.width", rect.width());
    params.SetByDottedPath("bounds.height", rect.height());
    params.SetByDottedPath("bounds.windowState", "normal");

    browser_devtools_client_.SendCommand(
        "Browser.setWindowBounds", std::move(params), std::move(callback));
  }

  void SetWindowState(const std::string& window_state,
                      SimpleDevToolsProtocolClient::ResponseCallback callback) {
    base::Value::Dict params;
    params.Set("windowId", window_id());
    params.SetByDottedPath("bounds.windowState", window_state);

    browser_devtools_client_.SendCommand(
        "Browser.setWindowBounds", std::move(params), std::move(callback));
  }

  void GetWindowBounds(
      SimpleDevToolsProtocolClient::ResponseCallback callback) {
    browser_devtools_client_.SendCommand("Browser.getWindowBounds",
                                         Param("windowId", window_id()),
                                         std::move(callback));
  }

  void CheckWindowBounds(const gfx::Rect& bounds,
                         const std::string window_state,
                         base::Value::Dict result) {
    gfx::Rect actual_bounds(DictInt(result, "result.bounds.left"),
                            DictInt(result, "result.bounds.top"),
                            DictInt(result, "result.bounds.width"),
                            DictInt(result, "result.bounds.height"));

    std::string actual_window_state =
        DictString(result, "result.bounds.windowState");

    // Mac does not support repositioning, as we don't show any actual window.
#if !BUILDFLAG(IS_MAC)
    EXPECT_EQ(bounds.x(), actual_bounds.x());
    EXPECT_EQ(bounds.y(), actual_bounds.y());
#endif  // !BUILDFLAG(IS_MAC)
    EXPECT_EQ(bounds.width(), actual_bounds.width());
    EXPECT_EQ(bounds.height(), actual_bounds.height());
    EXPECT_EQ(window_state, actual_window_state);
  }
};

class HeadlessDevToolsClientChangeWindowBoundsTest
    : public HeadlessDevToolsClientWindowManagementTest {
  gfx::Rect new_bounds() { return gfx::Rect(100, 200, 300, 400); }

  void RunDevTooledTest() override {
    SetWindowBounds(
        new_bounds(),
        base::BindOnce(
            &HeadlessDevToolsClientChangeWindowBoundsTest::OnSetWindowBounds,
            base::Unretained(this)));
  }

  void OnSetWindowBounds(base::Value::Dict result) {
    GetWindowBounds(base::BindOnce(
        &HeadlessDevToolsClientChangeWindowBoundsTest::OnGetWindowBounds,
        base::Unretained(this)));
  }

  void OnGetWindowBounds(base::Value::Dict result) {
    CheckWindowBounds(new_bounds(), "normal", std::move(result));
    FinishAsynchronousTest();
  }
};

#if BUILDFLAG(IS_MAC) && defined(ADDRESS_SANITIZER)
// TODO(crbug.com/1086872): Disabled due to flakiness on Mac ASAN.
DISABLED_HEADLESS_DEVTOOLED_TEST_F(
    HeadlessDevToolsClientChangeWindowBoundsTest);
#else
HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsClientChangeWindowBoundsTest);
#endif

class HeadlessDevToolsClientOuterSizeTest
    : public HeadlessDevToolsClientWindowManagementTest {
  void RunDevTooledTest() override {
    SetWindowBounds(
        gfx::Rect(100, 200, 800, 600),
        base::BindOnce(&HeadlessDevToolsClientOuterSizeTest::OnSetWindowBounds,
                       base::Unretained(this)));
  }

  void OnSetWindowBounds(base::Value::Dict) {
    EXPECT_EQ(800, Evaluate("window.outerWidth"));
    EXPECT_EQ(600, Evaluate("window.outerHeight"));

    FinishAsynchronousTest();
  }

  int Evaluate(const std::string& expression) {
    base::Value::Dict result = SendCommandSync(
        devtools_client_, "Runtime.evaluate", Param("expression", expression));
    return DictInt(result, "result.result.value");
  }
};

HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsClientOuterSizeTest);

class HeadlessDevToolsClientChangeWindowStateTest
    : public HeadlessDevToolsClientWindowManagementTest {
 public:
  explicit HeadlessDevToolsClientChangeWindowStateTest(
      const std::string& window_state)
      : window_state_(window_state) {}

  void RunDevTooledTest() override {
    SetWindowState(
        window_state_,
        base::BindOnce(
            &HeadlessDevToolsClientChangeWindowStateTest::OnSetWindowState,
            base::Unretained(this)));
  }

  void OnSetWindowState(base::Value::Dict) {
    GetWindowBounds(base::BindOnce(
        &HeadlessDevToolsClientChangeWindowStateTest::OnGetWindowState,
        base::Unretained(this)));
  }

  void OnGetWindowState(base::Value::Dict result) {
    HeadlessBrowser::Options::Builder builder;
    const HeadlessBrowser::Options kDefaultOptions = builder.Build();
    CheckWindowBounds(gfx::Rect(kDefaultOptions.window_size), window_state_,
                      std::move(result));
    FinishAsynchronousTest();
  }

 protected:
  std::string window_state_;
};

class HeadlessDevToolsClientMinimizeWindowTest
    : public HeadlessDevToolsClientChangeWindowStateTest {
 public:
  HeadlessDevToolsClientMinimizeWindowTest()
      : HeadlessDevToolsClientChangeWindowStateTest("minimized") {}
};

HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsClientMinimizeWindowTest);

class HeadlessDevToolsClientMaximizeWindowTest
    : public HeadlessDevToolsClientChangeWindowStateTest {
 public:
  HeadlessDevToolsClientMaximizeWindowTest()
      : HeadlessDevToolsClientChangeWindowStateTest("maximized") {}
};

HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsClientMaximizeWindowTest);

class HeadlessDevToolsClientFullscreenWindowTest
    : public HeadlessDevToolsClientChangeWindowStateTest {
 public:
  HeadlessDevToolsClientFullscreenWindowTest()
      : HeadlessDevToolsClientChangeWindowStateTest("fullscreen") {}
};

HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsClientFullscreenWindowTest);

class HeadlessDevToolsClientEvalTest : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    base::Value::Dict result = SendCommandSync(
        devtools_client_, "Runtime.evaluate", Param("expression", "1 + 2"));

    EXPECT_THAT(result, DictHasValue("result.result.value", 3));

    FinishAsynchronousTest();
  }
};

HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsClientEvalTest);

class HeadlessDevToolsNavigationControlTest
    : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Network.requestIntercepted",
        base::BindRepeating(
            &HeadlessDevToolsNavigationControlTest::OnRequestIntercepted,
            base::Unretained(this)));

    devtools_client_.AddEventHandler(
        "Page.frameStoppedLoading",
        base::BindRepeating(
            &HeadlessDevToolsNavigationControlTest::OnFrameStoppedLoading,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Page.enable");
    SendCommandSync(devtools_client_, "Network.enable");

    base::Value::List patterns;
    patterns.Append(Param("urlPattern", "*"));
    devtools_client_.SendCommand("Network.setRequestInterception",
                                 Param("patterns", std::move(patterns)));

    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url", embedded_test_server()->GetURL("/hello.html").spec()));
  }

  void OnRequestIntercepted(const base::Value::Dict& params) {
    if (DictBool(params, "params.isNavigationRequest"))
      navigation_requested_ = true;

    // Allow the navigation to proceed.
    devtools_client_.SendCommand(
        "Network.continueInterceptedRequest",
        Param("interceptionId", DictString(params, "params.interceptionId")));
  }

  void OnFrameStoppedLoading(const base::Value::Dict& params) {
    EXPECT_TRUE(navigation_requested_);
    FinishAsynchronousTest();
  }

 private:
  bool navigation_requested_ = false;
};

HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsNavigationControlTest);

class HeadlessCrashObserverTest : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    devtools_client_.AddEventHandler(
        "Inspector.targetCrashed",
        base::BindRepeating(&HeadlessCrashObserverTest::OnTargetCrashed,
                            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Inspector.enable");

    devtools_client_.SendCommand("Page.navigate",
                                 Param("url", blink::kChromeUICrashURL));
  }

  void OnTargetCrashed(const base::Value::Dict&) { FinishAsynchronousTest(); }

  // Make sure we don't fail because the renderer crashed!
  void RenderProcessExited(base::TerminationStatus status,
                           int exit_code) override {
#if BUILDFLAG(IS_WIN) && defined(ADDRESS_SANITIZER)
    // TODO(crbug.com/845011): Make ASan not interfere and expect a crash.
    // ASan's normal error exit code is 1, which base categorizes as the process
    // being killed.
    EXPECT_EQ(base::TERMINATION_STATUS_PROCESS_WAS_KILLED, status);
#elif BUILDFLAG(IS_WIN) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX) || \
    BUILDFLAG(IS_FUCHSIA)
    EXPECT_EQ(base::TERMINATION_STATUS_PROCESS_CRASHED, status);
#else
    EXPECT_EQ(base::TERMINATION_STATUS_ABNORMAL_TERMINATION, status);
#endif
  }

 private:
  content::ScopedAllowRendererCrashes scoped_allow_renderer_crashes_;
};

// TODO(1272554): HeadlessCrashObserverTest.RunAsyncTest is flaky on Win debug.
#if BUILDFLAG(IS_WIN) && !defined(NDEBUG)
DISABLED_HEADLESS_DEVTOOLED_TEST_F(HeadlessCrashObserverTest);
#else
HEADLESS_DEVTOOLED_TEST_F(HeadlessCrashObserverTest);
#endif

class HeadlessDevToolsNetworkBlockedUrlTest
    : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Network.requestWillBeSent",
        base::BindRepeating(
            &HeadlessDevToolsNetworkBlockedUrlTest::OnRequestWillBeSent,
            base::Unretained(this)));

    devtools_client_.AddEventHandler(
        "Network.responseReceived",
        base::BindRepeating(
            &HeadlessDevToolsNetworkBlockedUrlTest::OnResponseReceived,
            base::Unretained(this)));

    devtools_client_.AddEventHandler(
        "Network.loadingFailed",
        base::BindRepeating(
            &HeadlessDevToolsNetworkBlockedUrlTest::OnLoadingFailed,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Network.enable");

    devtools_client_.AddEventHandler(
        "Page.loadEventFired",
        base::BindRepeating(
            &HeadlessDevToolsNetworkBlockedUrlTest::OnLoadEventFired,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Page.enable");

    base::Value::List urls;
    urls.Append("dom_tree_test.css");
    devtools_client_.SendCommand("Network.setBlockedURLs",
                                 Param("urls", std::move(urls)));

    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url",
              embedded_test_server()->GetURL("/dom_tree_test.html").spec()));
  }

  std::string GetUrlPath(const std::string& url) const {
    GURL gurl(url);
    return gurl.path();
  }

  void OnRequestWillBeSent(const base::Value::Dict& params) {
    std::string path = GetUrlPath(DictString(params, "params.request.url"));
    requests_to_be_sent_.push_back(path);
    request_id_to_path_[DictString(params, "params.requestId")] = path;
  }

  void OnResponseReceived(const base::Value::Dict& params) {
    responses_received_.push_back(
        GetUrlPath(DictString(params, "params.response.url")));
  }

  void OnLoadingFailed(const base::Value::Dict& params) {
    failures_.push_back(
        request_id_to_path_[DictString(params, "params.requestId")]);
    EXPECT_EQ("inspector", DictString(params, "params.blockedReason"));
  }

  void OnLoadEventFired(const base::Value::Dict&) {
    EXPECT_THAT(
        requests_to_be_sent_,
        testing::UnorderedElementsAre("/dom_tree_test.html",
                                      "/dom_tree_test.css", "/iframe.html"));
    EXPECT_THAT(responses_received_,
                ElementsAre("/dom_tree_test.html", "/iframe.html"));
    EXPECT_THAT(failures_, ElementsAre("/dom_tree_test.css"));

    FinishAsynchronousTest();
  }

  std::map<std::string, std::string> request_id_to_path_;
  std::vector<std::string> requests_to_be_sent_;
  std::vector<std::string> responses_received_;
  std::vector<std::string> failures_;
};

HEADLESS_DEVTOOLED_TEST_F(HeadlessDevToolsNetworkBlockedUrlTest);

class DevToolsNetworkOfflineEmulationTest
    : public HeadlessDevTooledBrowserTest {
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Network.loadingFailed",
        base::BindRepeating(
            &DevToolsNetworkOfflineEmulationTest::OnLoadingFailed,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Network.enable");

    base::Value::Dict params;
    params.Set("offline", true);
    params.Set("latency", 0);
    params.Set("downloadThroughput", 0);
    params.Set("uploadThroughput", 0);
    devtools_client_.SendCommand("Network.emulateNetworkConditions",
                                 std::move(params));

    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url", embedded_test_server()->GetURL("/hello.html").spec()));
  }

  void OnLoadingFailed(const base::Value::Dict& params) {
    EXPECT_THAT(params, DictHasValue("params.errorText",
                                     "net::ERR_INTERNET_DISCONNECTED"));
    EXPECT_EQ("net::ERR_INTERNET_DISCONNECTED",
              DictString(params, "params.errorText"));

    FinishAsynchronousTest();
  }
};

HEADLESS_DEVTOOLED_TEST_F(DevToolsNetworkOfflineEmulationTest);

class DomTreeExtractionBrowserTest : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Page.loadEventFired",
        base::BindRepeating(&DomTreeExtractionBrowserTest::OnLoadEventFired,
                            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Page.enable");

    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url",
              embedded_test_server()->GetURL("/dom_tree_test.html").spec()));
  }

  void OnLoadEventFired(const base::Value::Dict&) {
    SendCommandSync(devtools_client_, "Page.disable");

    base::Value::List css_whitelist;
    css_whitelist.Append("color");
    css_whitelist.Append("display");
    css_whitelist.Append("font-style");
    css_whitelist.Append("font-family");
    css_whitelist.Append("margin-left");
    css_whitelist.Append("margin-right");
    css_whitelist.Append("margin-top");
    css_whitelist.Append("margin-bottom");
    devtools_client_.SendCommand(
        "DOMSnapshot.getSnapshot",
        Param("computedStyleWhitelist", css_whitelist),
        base::BindOnce(&DomTreeExtractionBrowserTest::OnGetSnapshotDone,
                       base::Unretained(this)));
  }

  void OnGetSnapshotDone(base::Value::Dict result) {
    std::vector<base::Value::Dict> dom_nodes;
    GetDomNodes(result, &dom_nodes);

    std::vector<base::Value::Dict> computed_styles;
    GetComputedStyles(result, &computed_styles);

    base::ScopedAllowBlockingForTesting allow_blocking;
    base::FilePath source_root_dir;
    base::PathService::Get(base::DIR_SOURCE_ROOT, &source_root_dir);

    CompareToGolden(
        dom_nodes,
        source_root_dir.Append(FILE_PATH_LITERAL(
            "headless/test/dom_tree_extraction_expected_nodes.txt")));

    CompareToGolden(
        computed_styles,
        source_root_dir.Append(FILE_PATH_LITERAL(
            "headless/test/dom_tree_extraction_expected_styles.txt")));

    FinishAsynchronousTest();
  }

  void GetDomNodes(const base::Value::Dict& result,
                   std::vector<base::Value::Dict>* dom_nodes) {
    GURL::Replacements replace_port;
    replace_port.SetPortStr("");

    const base::Value::List* dom_nodes_list =
        result.FindListByDottedPath("result.domNodes");
    ASSERT_NE(dom_nodes_list, nullptr);
    ASSERT_GT(dom_nodes_list->size(), 0ul);

    const base::Value::List* layout_tree_nodes_list =
        result.FindListByDottedPath("result.layoutTreeNodes");
    ASSERT_NE(layout_tree_nodes_list, nullptr);
    ASSERT_GT(layout_tree_nodes_list->size(), 0ul);

    // For convenience, flatten the dom tree into an array of dicts.
    dom_nodes->reserve(dom_nodes_list->size());
    for (const auto& dom_node : *dom_nodes_list) {
      ASSERT_TRUE(dom_node.is_dict());
      dom_nodes->push_back(dom_node.GetDict().Clone());
      base::Value::Dict& node_dict = dom_nodes->back();

      // Node IDs are assigned in a non deterministic way.
      if (node_dict.Find("backendNodeId"))
        node_dict.Set("backendNodeId", "?");

      // Frame IDs are random.
      if (node_dict.Find("frameId"))
        node_dict.Set("frameId", "?");

      // Ports are random.
      if (base::Value* base_url_value = node_dict.Find("baseURL")) {
        node_dict.Set("baseURL", GURL(base_url_value->GetString())
                                     .ReplaceComponents(replace_port)
                                     .spec());
      }

      if (base::Value* document_url_value = node_dict.Find("documentURL")) {
        node_dict.Set("documentURL", GURL(document_url_value->GetString())
                                         .ReplaceComponents(replace_port)
                                         .spec());
      }

      // Golden file expects scrollOffsetXY to have fractional part.
      // TODO(kvitekp): Consider updating golden files.
      if (absl::optional<double> x = node_dict.FindDouble("scrollOffsetX"))
        node_dict.Set("scrollOffsetX", *x);

      if (absl::optional<double> y = node_dict.FindDouble("scrollOffsetY"))
        node_dict.Set("scrollOffsetY", *y);

      // Merge LayoutTreeNode data into the dom_node dictionary.
      if (absl::optional<int> layout_node_index =
              node_dict.FindInt("layoutNodeIndex")) {
        ASSERT_LE(0, *layout_node_index);
        ASSERT_GT(layout_tree_nodes_list->size(),
                  static_cast<size_t>(*layout_node_index));

        const base::Value::Dict& layout_tree_node =
            (*layout_tree_nodes_list)[*layout_node_index].GetDict();

        if (const base::Value::Dict* bounding_box =
                layout_tree_node.FindDict("boundingBox")) {
          node_dict.Set("boundingBox", bounding_box->Clone());
          FixBoundingBox(node_dict);
        }

        if (const std::string* layout_text =
                layout_tree_node.FindString("layoutText")) {
          node_dict.Set("layoutText", *layout_text);
        }

        if (absl::optional<int> style_index =
                layout_tree_node.FindInt("styleIndex")) {
          node_dict.Set("styleIndex", *style_index);
        }

        if (const base::Value::List* inline_text_nodes =
                layout_tree_node.FindList("inlineTextNodes")) {
          base::Value::List list = inline_text_nodes->Clone();
          for (auto& list_entry : list) {
            ASSERT_TRUE(list_entry.is_dict());
            FixBoundingBox(list_entry.GetDict());
          }
          node_dict.Set("inlineTextNodes", std::move(list));
        }
      }
    }
  }

  void FixBoundingBox(base::Value::Dict& dict) {
    if (base::Value::Dict* bounding_box = dict.FindDict("boundingBox")) {
      // The golden file expects double values in boundingBox.
      // TODO(kvitekp): Consider updating the golden and just
      // cloning the |boundingBox| dictionary here.
      bounding_box->Set("x", *bounding_box->FindDouble("x"));
      bounding_box->Set("y", *bounding_box->FindDouble("y"));
      bounding_box->Set("width", *bounding_box->FindDouble("width"));
      bounding_box->Set("height", *bounding_box->FindDouble("height"));
    }
  }

  void GetComputedStyles(const base::Value::Dict& result,
                         std::vector<base::Value::Dict>* computed_styles) {
    const base::Value::List* computed_styles_list =
        result.FindListByDottedPath("result.computedStyles");
    ASSERT_NE(computed_styles_list, nullptr);
    ASSERT_GT(computed_styles_list->size(), 0ul);

    computed_styles->reserve(computed_styles_list->size());
    for (const auto& computed_style : *computed_styles_list) {
      ASSERT_TRUE(computed_style.is_dict());
      const base::Value::List* properties_list =
          computed_style.GetDict().FindList("properties");
      ASSERT_NE(properties_list, nullptr);

      base::Value::Dict computed_style_dict;
      for (const auto& property : *properties_list) {
        ASSERT_TRUE(property.is_dict());
        const base::Value::Dict& property_dict = property.GetDict();
        computed_style_dict.Set(*property_dict.FindString("name"),
                                *property_dict.FindString("value"));
      }
      computed_styles->push_back(std::move(computed_style_dict));
    }
  }

  void CompareToGolden(const std::vector<base::Value::Dict>& entries,
                       base::FilePath expected_filepath) {
    std::string expected_entries;
    ASSERT_TRUE(base::ReadFileToString(expected_filepath, &expected_entries));

    std::string actual_entries;
    for (const base::Value::Dict& entry : entries) {
      std::string entry_json;
      base::JSONWriter::WriteWithOptions(
          entry, base::JSONWriter::OPTIONS_PRETTY_PRINT, &entry_json);
      actual_entries += entry_json;
    }

#if BUILDFLAG(IS_WIN)
    ASSERT_TRUE(base::RemoveChars(actual_entries, "\r", &actual_entries));
#endif

    EXPECT_EQ(expected_entries, actual_entries);
  }
};

// TODO(crbug.com/1090930): Fix this test on Fuchsia and re-enable.
// NOTE: These macros expand to: DomTreeExtractionBrowserTest.RunAsyncTest
#if BUILDFLAG(IS_FUCHSIA)
DISABLED_HEADLESS_DEVTOOLED_TEST_F(DomTreeExtractionBrowserTest);
#else
HEADLESS_DEVTOOLED_TEST_F(DomTreeExtractionBrowserTest);
#endif

class BlockedByClient_NetworkObserver_Test
    : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Network.requestIntercepted",
        base::BindRepeating(
            &BlockedByClient_NetworkObserver_Test::OnRequestIntercepted,
            base::Unretained(this)));

    devtools_client_.AddEventHandler(
        "Network.requestWillBeSent",
        base::BindRepeating(
            &BlockedByClient_NetworkObserver_Test::OnRequestWillBeSent,
            base::Unretained(this)));

    devtools_client_.AddEventHandler(
        "Network.loadingFailed",
        base::BindRepeating(
            &BlockedByClient_NetworkObserver_Test::OnLoadingFailed,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Network.enable");

    // Intercept all network requests.
    base::Value::List patterns;
    patterns.Append(Param("urlPattern", "*"));
    devtools_client_.SendCommand("Network.setRequestInterception",
                                 Param("patterns", std::move(patterns)));

    devtools_client_.AddEventHandler(
        "Page.loadEventFired",
        base::BindRepeating(
            &BlockedByClient_NetworkObserver_Test::OnLoadEventFired,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Page.enable");

    devtools_client_.SendCommand(
        "Page.navigate", Param("url", embedded_test_server()
                                          ->GetURL("/resource_cancel_test.html")
                                          .spec()));
  }

  void OnRequestIntercepted(const base::Value::Dict& params) {
    const std::string url = DictString(params, "params.request.url");

    urls_seen_.push_back(GURL(url).ExtractFileName());

    base::Value::Dict continue_intercept_params;
    continue_intercept_params.Set("interceptionId",
                                  DictString(params, "params.interceptionId"));

    // TODO(kvitekp): comment does not match the following code, review!!!

    // We *abort* fetching Ahem.ttf, and *fail* for test.jpg
    // to verify that both ways result in a failed loading event,
    // which we'll observe in OnLoadingFailed below.
    // Also, we abort iframe2.html because it turns out frame interception
    // uses a very different codepath than other resources.
    if (EndsWith(url, "/test.jpg", base::CompareCase::SENSITIVE)) {
      continue_intercept_params.Set("errorReason", "BlockedByClient");
    } else if (EndsWith(url, "/Ahem.ttf", base::CompareCase::SENSITIVE)) {
      continue_intercept_params.Set("errorReason", "BlockedByClient");
    } else if (EndsWith(url, "/iframe2.html", base::CompareCase::SENSITIVE)) {
      continue_intercept_params.Set("errorReason", "BlockedByClient");
    }

    devtools_client_.SendCommand("Network.continueInterceptedRequest",
                                 std::move(continue_intercept_params));
  }

  void OnRequestWillBeSent(const base::Value::Dict& params) {
    // Here, we just record the URLs (filenames) for each request ID, since
    // we won't have access to them in OnLoadingFailed below.
    urls_by_id_[DictString(params, "params.requestId")] =
        GURL(DictString(params, "params.request.url")).ExtractFileName();
  }

  void OnLoadingFailed(const base::Value::Dict& params) {
    // Record the failed loading events so we can verify below that we
    // received the events.
    urls_that_failed_to_load_.push_back(
        urls_by_id_[DictString(params, "params.requestId")]);
    EXPECT_EQ("inspector", DictString(params, "params.blockedReason"));
  }

  void OnLoadEventFired(const base::Value::Dict&) {
    EXPECT_THAT(urls_that_failed_to_load_,
                UnorderedElementsAre("test.jpg", "Ahem.ttf", "iframe2.html"));
    EXPECT_THAT(urls_seen_, UnorderedElementsAre("resource_cancel_test.html",
                                                 "dom_tree_test.css",
                                                 "test.jpg", "iframe.html",
                                                 "iframe2.html", "Ahem.ttf"));

    FinishAsynchronousTest();
  }

 private:
  std::vector<std::string> urls_seen_;
  std::vector<std::string> urls_that_failed_to_load_;
  std::map<std::string, std::string> urls_by_id_;
};

HEADLESS_DEVTOOLED_TEST_F(BlockedByClient_NetworkObserver_Test);

class DevtoolsInterceptionWithAuthProxyTest
    : public HeadlessDevTooledBrowserTest {
 public:
  DevtoolsInterceptionWithAuthProxyTest()
      : proxy_server_(net::SpawnedTestServer::TYPE_BASIC_AUTH_PROXY,
                      base::FilePath(FILE_PATH_LITERAL("headless/test/data"))) {
  }

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

  void RunDevTooledTest() override {
    ASSERT_TRUE(embedded_test_server()->Start());

    devtools_client_.AddEventHandler(
        "Network.requestIntercepted",
        base::BindRepeating(
            &DevtoolsInterceptionWithAuthProxyTest::OnRequestIntercepted,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Network.enable");

    // Intercept all network requests.
    base::Value::List patterns;
    patterns.Append(Param("urlPattern", "*"));
    devtools_client_.SendCommand("Network.setRequestInterception",
                                 Param("patterns", std::move(patterns)));

    devtools_client_.AddEventHandler(
        "Page.loadEventFired",
        base::BindRepeating(
            &DevtoolsInterceptionWithAuthProxyTest::OnLoadEventFired,
            base::Unretained(this)));

    SendCommandSync(devtools_client_, "Page.enable");

    devtools_client_.SendCommand(
        "Page.navigate",
        Param("url",
              embedded_test_server()->GetURL("/dom_tree_test.html").spec()));
  }

  void OnRequestIntercepted(const base::Value::Dict& params) {
    base::Value::Dict continue_intercept_params;
    continue_intercept_params.Set("interceptionId",
                                  DictString(params, "params.interceptionId"));

    if (DictHas(params, "params.authChallenge")) {
      auth_challenge_seen_ = true;

      base::Value::Dict auth_challenge_response;
      auth_challenge_response.Set("response", "ProvideCredentials");
      auth_challenge_response.Set("username", "foo");
      auth_challenge_response.Set("password", "bar");
      continue_intercept_params.Set("authChallengeResponse",
                                    std::move(auth_challenge_response));
    } else {
      GURL url(DictString(params, "params.request.url"));
      files_loaded_.insert(url.path());
    }

    devtools_client_.SendCommand("Network.continueInterceptedRequest",
                                 std::move(continue_intercept_params));
  }

  void OnLoadEventFired(const base::Value::Dict&) {
    EXPECT_TRUE(auth_challenge_seen_);
    EXPECT_THAT(files_loaded_,
                ElementsAre("/Ahem.ttf", "/dom_tree_test.css",
                            "/dom_tree_test.html", "/iframe.html"));

    FinishAsynchronousTest();
  }

  void CustomizeHeadlessBrowserContext(
      HeadlessBrowserContext::Builder& builder) override {
    std::unique_ptr<net::ProxyConfig> proxy_config(new net::ProxyConfig);
    proxy_config->proxy_rules().ParseFromString(
        proxy_server_.host_port_pair().ToString());
    // TODO(https://crbug.com/901896): Don't rely on proxying localhost.
    proxy_config->proxy_rules().bypass_rules.AddRulesToSubtractImplicit();
    builder.SetProxyConfig(std::move(proxy_config));
  }

 private:
  net::SpawnedTestServer proxy_server_;
  bool auth_challenge_seen_ = false;
  std::set<std::string> files_loaded_;
};

#if (BUILDFLAG(IS_MAC) && defined(ADDRESS_SANITIZER)) || BUILDFLAG(IS_FUCHSIA)
// TODO(crbug.com/1086872): Disabled due to flakiness on Mac ASAN.
// TODO(crbug.com/1090933): Reenable on Fuchsia when fixed.
// NOTE: This macro expands to:
//   DevtoolsInterceptionWithAuthProxyTest.RunAsyncTest
DISABLED_HEADLESS_DEVTOOLED_TEST_F(DevtoolsInterceptionWithAuthProxyTest);
#else
HEADLESS_DEVTOOLED_TEST_F(DevtoolsInterceptionWithAuthProxyTest);
#endif

class NavigatorLanguages : public HeadlessDevTooledBrowserTest {
 public:
  void RunDevTooledTest() override {
    devtools_client_.SendCommand(
        "Runtime.evaluate",
        Param("expression", "JSON.stringify(navigator.languages)"),
        base::BindOnce(&NavigatorLanguages::OnEvaluateResult,
                       base::Unretained(this)));
  }

  void OnEvaluateResult(base::Value::Dict result) {
    EXPECT_THAT(result, DictHasValue("result.result.value",
                                     "[\"en-UK\",\"DE\",\"FR\"]"));
    FinishAsynchronousTest();
  }

  void CustomizeHeadlessBrowserContext(
      HeadlessBrowserContext::Builder& builder) override {
    builder.SetAcceptLanguage("en-UK, DE, FR");
  }
};

HEADLESS_DEVTOOLED_TEST_F(NavigatorLanguages);

class AcceptLanguagesSwitch : public HeadlessDevTooledBrowserTest {
 public:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    HeadlessDevTooledBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendSwitchASCII(switches::kAcceptLang, "cz-CZ");
  }

  void RunDevTooledTest() override {
    devtools_client_.SendCommand(
        "Runtime.evaluate",
        Param("expression", "JSON.stringify(navigator.languages)"),
        base::BindOnce(&AcceptLanguagesSwitch::OnEvaluateResult,
                       base::Unretained(this)));
  }

  void OnEvaluateResult(base::Value::Dict result) {
    EXPECT_THAT(result, DictHasValue("result.result.value", "[\"cz-CZ\"]"));
    FinishAsynchronousTest();
  }
};

HEADLESS_DEVTOOLED_TEST_F(AcceptLanguagesSwitch);

}  // namespace headless