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

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

#include "base/command_line.h"
#include "base/containers/span.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/functional/bind.h"
#include "base/logging.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/string_util.h"
#include "base/strings/to_string.h"
#include "base/test/test_timeouts.h"
#include "base/test/values_test_util.h"
#include "base/threading/platform_thread.h"
#include "base/values.h"
#include "build/build_config.h"
#include "chrome/browser/headless/test/headless_mode_browsertest.h"
#include "chrome/browser/ui/browser_window/public/browser_collection_observer.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/common/chrome_switches.h"
#include "components/headless/command_handler/headless_command_handler.h"
#include "components/headless/command_handler/headless_command_switches.h"
#include "components/headless/test/bitmap_utils.h"
#include "components/headless/test/capture_std_stream.h"
#include "components/headless/test/pdf_utils.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "pdf/pdf.h"
#include "printing/buildflags/buildflags.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/display/display_switches.h"
#include "ui/gfx/codec/png_codec.h"
#include "url/gurl.h"

namespace headless {

class HeadlessModeCommandBrowserTest : public HeadlessModeBrowserTest,
                                       public BrowserCollectionObserver {
 public:
  HeadlessModeCommandBrowserTest() = default;

  void PreRunTestOnMainThread() override {
    if (auto* collection = GlobalBrowserCollection::GetInstance()) {
      browser_collection_observation_.Observe(collection);
    }
    HeadlessModeBrowserTest::PreRunTestOnMainThread();
  }

  void TearDownOnMainThread() override {
    browser_collection_observation_.Reset();
    HeadlessModeBrowserTest::TearDownOnMainThread();
  }

  void OnBrowserClosed(BrowserWindowInterface* browser) override {
    // Unconditionally clear browser_ because Headless mode command tests
    // shouldn't depend on the default browser. This avoids MSAN use-after-free
    // if the browser is closed during PreRunTestOnMainThread's message loop.
    SetBrowser(nullptr);
  }

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

    HeadlessCommandHandler::SetDoneCallbackForTesting(base::BindOnce(
        &HeadlessModeCommandBrowserTest::FinishTest, base::Unretained(this)));

    HeadlessModeBrowserTest::SetUp();
  }

 protected:
  // Override this to provide the test specific target page.
  virtual std::string GetTargetPage() { return "/hello.html"; }

  GURL GetTargetUrl(const std::string& url) {
    return embedded_test_server()->GetURL(url);
  }

  std::optional<HeadlessCommandHandler::Result> ProcessCommands() {
    if (!test_complete_) {
      run_loop_ = std::make_unique<base::RunLoop>();
      run_loop_->Run();
      run_loop_.reset();
    }

    return result_;
  }

 private:
  void FinishTest(HeadlessCommandHandler::Result result) {
    result_ = result;
    test_complete_ = true;
    if (run_loop_) {
      run_loop_->Quit();
    }
  }

  std::unique_ptr<base::RunLoop> run_loop_;
  bool test_complete_ = false;
  std::optional<HeadlessCommandHandler::Result> result_;
  base::ScopedObservation<GlobalBrowserCollection, BrowserCollectionObserver>
      browser_collection_observation_{this};
};

#define HEADLESS_MODE_COMMAND_BROWSER_TEST_WITH_TARGET_URL(                \
    test_fixture, test_name, target_url)                                   \
  class HeadlessModeCommandBrowserTest_##test_name : public test_fixture { \
   public:                                                                 \
    HeadlessModeCommandBrowserTest_##test_name() = default;                \
    std::string GetTargetPage() override {                                 \
      return target_url;                                                   \
    }                                                                      \
  };                                                                       \
  IN_PROC_BROWSER_TEST_F(HeadlessModeCommandBrowserTest_##test_name, test_name)

class HeadlessModeCommandBrowserTestWithTempDir
    : public HeadlessModeCommandBrowserTest {
 public:
  HeadlessModeCommandBrowserTestWithTempDir() = default;

  void SetUp() override {
    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
    ASSERT_TRUE(base::IsDirectoryEmpty(temp_dir()));

    HeadlessModeCommandBrowserTest::SetUp();
  }

  void TearDown() override {
    HeadlessModeCommandBrowserTest::TearDown();

    ASSERT_TRUE(temp_dir_.Delete());
  }

  const base::FilePath& temp_dir() const { return temp_dir_.GetPath(); }

 private:
  base::ScopedTempDir temp_dir_;
};

// DumpDom command tests ----------------------------------------------

class HeadlessModeDumpDomCommandBrowserTestBase
    : public HeadlessModeCommandBrowserTest {
 public:
  HeadlessModeDumpDomCommandBrowserTestBase() = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    HeadlessModeCommandBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendSwitch(switches::kDumpDom);
    command_line->AppendArg(GetTargetUrl(GetTargetPage()).spec());

    capture_stdout_.StartCapture();
  }

 protected:
  CaptureStdOut capture_stdout_;
};

class HeadlessModeDumpDomCommandBrowserTest
    : public HeadlessModeDumpDomCommandBrowserTestBase,
      public testing::WithParamInterface<bool> {
 public:
  HeadlessModeDumpDomCommandBrowserTest() = default;

 private:
  bool IsIncognito() override { return GetParam(); }
};

INSTANTIATE_TEST_SUITE_P(/* no prefix */,
                         HeadlessModeDumpDomCommandBrowserTest,
                         ::testing::Bool());

// TODO(crbug.com/40266323): Reenable once deflaked.
// TODO(crbug.com/514143472): Reenable once deflaked.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
#define MAYBE_HeadlessDumpDom DISABLED_HeadlessDumpDom
#else
#define MAYBE_HeadlessDumpDom HeadlessDumpDom
#endif
IN_PROC_BROWSER_TEST_P(HeadlessModeDumpDomCommandBrowserTest,
                       MAYBE_HeadlessDumpDom) {
  ASSERT_THAT(ProcessCommands(),
              testing::Eq(HeadlessCommandHandler::Result::kSuccess));

  capture_stdout_.StopCapture();
  std::string captured_stdout = capture_stdout_.TakeCapturedData();

  static const char kDomDump[] =
      "<!DOCTYPE html>\n"
      "<html><head></head><body><h1>Hello headless world!</h1>\n"
      "</body></html>\n";
  EXPECT_THAT(captured_stdout, testing::HasSubstr(kDomDump));
}

class HeadlessModeDumpDomNonExistingPageCommandBrowserTest
    : public HeadlessModeCommandBrowserTest {
 public:
  HeadlessModeDumpDomNonExistingPageCommandBrowserTest() = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    HeadlessModeCommandBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendSwitch(switches::kDumpDom);
    command_line->AppendArg("http://does-not-exist.test/hello.html");

    capture_stdout_.StartCapture();
  }

 protected:
  CaptureStdOut capture_stdout_;
};

IN_PROC_BROWSER_TEST_F(HeadlessModeDumpDomNonExistingPageCommandBrowserTest,
                       HeadlessDumpDomNonExistingPage) {
  EXPECT_THAT(ProcessCommands(),
              testing::Eq(HeadlessCommandHandler::Result::kPageLoadError));

  capture_stdout_.StopCapture();
  EXPECT_THAT(capture_stdout_.TakeCapturedData(), testing::IsEmpty());
}

class HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase
    : public HeadlessModeDumpDomCommandBrowserTestBase {
 public:
  HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase() = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    HeadlessModeDumpDomCommandBrowserTestBase::SetUpCommandLine(command_line);
    command_line->AppendSwitchASCII(switches::kTimeout,
                                    base::ToString(timeout().InMilliseconds()));
  }

  base::TimeDelta timeout() { return TestTimeouts::action_timeout(); }
};

class HeadlessModeDumpDomCommandBrowserTestWithTimeout
    : public HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase {
 public:
  HeadlessModeDumpDomCommandBrowserTestWithTimeout() = default;

  void SetUp() override {
    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
        &HeadlessModeDumpDomCommandBrowserTestWithTimeout::RequestHandler,
        base::Unretained(this)));

    HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase::SetUp();
  }

 private:
  std::string GetTargetPage() override { return "/page.html"; }

  std::unique_ptr<net::test_server::HttpResponse> RequestHandler(
      const net::test_server::HttpRequest& request) {
    if (request.relative_url == "/page.html") {
      auto response = std::make_unique<net::test_server::DelayedHttpResponse>(
          timeout() * 2);
      response->set_code(net::HTTP_OK);
      response->set_content_type("text/html");
      response->set_content(R"(<div>Hi, I'm headless!</div>)");

      return response;
    }

    return nullptr;
  }
};

IN_PROC_BROWSER_TEST_F(HeadlessModeDumpDomCommandBrowserTestWithTimeout,
                       HeadlessDumpDomWithTimeout) {
  // Main page timeout should be reported.
  EXPECT_THAT(ProcessCommands(),
              testing::Eq(HeadlessCommandHandler::Result::kPageLoadTimeout));

  capture_stdout_.StopCapture();
  std::string captured_stdout = capture_stdout_.TakeCapturedData();
  std::erase_if(captured_stdout, isspace);

  // Expect about:blank page DOM.
  EXPECT_THAT(captured_stdout,
              testing::HasSubstr("<html><head></head><body></body></html>"));
}

class HeadlessModeDumpDomCommandBrowserTestWithSubResourceTimeout
    : public HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase,
      public testing::WithParamInterface<bool> {
 public:
  HeadlessModeDumpDomCommandBrowserTestWithSubResourceTimeout() = default;

  bool delay_response() { return GetParam(); }

  void SetUp() override {
    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
        &HeadlessModeDumpDomCommandBrowserTestWithSubResourceTimeout::
            RequestHandler,
        base::Unretained(this)));

    HeadlessModeDumpDomCommandBrowserTestWithTimeoutBase::SetUp();
  }

 private:
  std::string GetTargetPage() override { return "/page.html"; }

  std::unique_ptr<net::test_server::HttpResponse> RequestHandler(
      const net::test_server::HttpRequest& request) {
    if (request.relative_url == "/page.html") {
      auto response = std::make_unique<net::test_server::BasicHttpResponse>();
      response->set_code(net::HTTP_OK);
      response->set_content_type("text/html");
      response->set_content(R"(
        <html><body>
        <div id="thediv">INITIAL</div>
        <script src="./script.js"></script>
        </body></html>
      )");

      return response;
    }

    if (request.relative_url == "/script.js") {
      std::unique_ptr<net::test_server::BasicHttpResponse> response;
      if (delay_response()) {
        response = std::make_unique<net::test_server::DelayedHttpResponse>(
            timeout() * 2);
      } else {
        response = std::make_unique<net::test_server::BasicHttpResponse>();
      }

      response->set_code(net::HTTP_OK);
      response->set_content_type("text/javascript");
      response->set_content(R"(
        document.getElementById("thediv").innerText="REPLACED";
      )");

      return response;
    }

    return nullptr;
  }
};

INSTANTIATE_TEST_SUITE_P(
    All,
    HeadlessModeDumpDomCommandBrowserTestWithSubResourceTimeout,
    testing::Bool());

// TODO(crbug.com/514143472): Reenable once deflaked.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
#define MAYBE_HeadlessDumpDomWithSubResourceTimeout \
  DISABLED_HeadlessDumpDomWithSubResourceTimeout
#else
#define MAYBE_HeadlessDumpDomWithSubResourceTimeout \
  HeadlessDumpDomWithSubResourceTimeout
#endif
IN_PROC_BROWSER_TEST_P(
    HeadlessModeDumpDomCommandBrowserTestWithSubResourceTimeout,
    MAYBE_HeadlessDumpDomWithSubResourceTimeout) {
  std::optional<HeadlessCommandHandler::Result> result = ProcessCommands();

  capture_stdout_.StopCapture();
  std::string captured_stdout = capture_stdout_.TakeCapturedData();
  std::erase_if(captured_stdout, isspace);

  if (delay_response()) {
    EXPECT_THAT(result,
                testing::Eq(HeadlessCommandHandler::Result::kPageLoadTimeout));
    EXPECT_THAT(captured_stdout,
                testing::HasSubstr(
                    "<html><head></head><body><divid=\"thediv\">INITIAL</"
                    "div><scriptsrc=\"./script.js\"></script></body></html>"));
  } else {
    EXPECT_THAT(result, testing::Eq(HeadlessCommandHandler::Result::kSuccess));
    EXPECT_THAT(captured_stdout,
                testing::HasSubstr(
                    "<html><head></head><body><divid=\"thediv\">REPLACED</"
                    "div><scriptsrc=\"./script.js\"></script></body></html>"));
  }
}

// TODO(crbug.com/505163310): Reenable once deflaked.
// TODO(crbug.com/514143472): Reenable once deflaked.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
#define MAYBE_DumpDomWithBeforeUnloadPreventDefault \
  DISABLED_DumpDomWithBeforeUnloadPreventDefault
#else
#define MAYBE_DumpDomWithBeforeUnloadPreventDefault \
  DumpDomWithBeforeUnloadPreventDefault
#endif
HEADLESS_MODE_COMMAND_BROWSER_TEST_WITH_TARGET_URL(
    HeadlessModeDumpDomCommandBrowserTestBase,
    MAYBE_DumpDomWithBeforeUnloadPreventDefault,
    "/before_unload_prevent_default.html") {
  // Make sure that 'beforeunload' that prevents default action does not stall
  // the command processing. The "Leave site" popup should not appear because
  // command target was not user activated.
  EXPECT_THAT(ProcessCommands(),
              testing::Eq(HeadlessCommandHandler::Result::kSuccess));
}

// Screenshot command tests -------------------------------------------

class HeadlessModeScreenshotCommandBrowserTest
    : public HeadlessModeCommandBrowserTestWithTempDir {
 public:
  HeadlessModeScreenshotCommandBrowserTest() = default;

#if BUILDFLAG(IS_WIN)
  void SetUp() override {
    // Use software compositing instead of GL which causes blank screenshots on
    // Windows, especially under ASAN. See https://crbug.com/40267033 and
    // https://crbug.com/328195816.
    UseSoftwareCompositing();

    HeadlessModeCommandBrowserTestWithTempDir::SetUp();
  }
#endif

  void SetUpCommandLine(base::CommandLine* command_line) override {
    HeadlessModeCommandBrowserTestWithTempDir::SetUpCommandLine(command_line);

    screenshot_filename_ =
        temp_dir().Append(FILE_PATH_LITERAL("screenshot.png"));
    command_line->AppendSwitchPath(switches::kScreenshot, screenshot_filename_);
    command_line->AppendArg(GetTargetUrl("/centered_blue_box.html").spec());
  }

 protected:
  base::FilePath screenshot_filename_;
};

// TODO(crbug.com/517342172): Re-enable this test on Linux MSAN.
#if BUILDFLAG(IS_LINUX) && defined(MEMORY_SANITIZER)
#define MAYBE_HeadlessScreenshot DISABLED_HeadlessScreenshot
#else
#define MAYBE_HeadlessScreenshot HeadlessScreenshot
#endif
IN_PROC_BROWSER_TEST_F(HeadlessModeScreenshotCommandBrowserTest,
                       MAYBE_HeadlessScreenshot) {
  ASSERT_THAT(ProcessCommands(),
              testing::Eq(HeadlessCommandHandler::Result::kSuccess));

  base::ScopedAllowBlockingForTesting allow_blocking;

  std::optional<std::vector<uint8_t>> png_data =
      base::ReadFileToBytes(screenshot_filename_);

  SkBitmap bitmap = gfx::PNGCodec::Decode(png_data.value());
  ASSERT_FALSE(bitmap.isNull());

  // Expect a centered blue rectangle on white background.
  EXPECT_TRUE(CheckColoredRect(bitmap, SkColorSetRGB(0x00, 0x00, 0xff),
                               SkColorSetRGB(0xff, 0xff, 0xff)));
}

class HeadlessModeScreenshotCommandWithWindowSizeBrowserTest
    : public HeadlessModeScreenshotCommandBrowserTest {
 public:
  HeadlessModeScreenshotCommandWithWindowSizeBrowserTest() = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    HeadlessModeScreenshotCommandBrowserTest::SetUpCommandLine(command_line);

    command_line->AppendSwitchASCII(::switches::kWindowSize, "2345,1234");
    command_line->AppendSwitchASCII(::switches::kForceDeviceScaleFactor, "1");
  }
};

IN_PROC_BROWSER_TEST_F(HeadlessModeScreenshotCommandWithWindowSizeBrowserTest,
                       HeadlessScreenshotWithWindowSize) {
  ASSERT_THAT(ProcessCommands(),
              testing::Eq(HeadlessCommandHandler::Result::kSuccess));

  base::ScopedAllowBlockingForTesting allow_blocking;

  std::optional<std::vector<uint8_t>> png_data =
      base::ReadFileToBytes(screenshot_filename_);

  SkBitmap bitmap = gfx::PNGCodec::Decode(png_data.value());
  ASSERT_FALSE(bitmap.isNull());

  EXPECT_EQ(bitmap.width(), 2345);
  EXPECT_EQ(bitmap.height(), 1234);

  // Expect a centered blue rectangle on white background.
  EXPECT_TRUE(CheckColoredRect(bitmap, SkColorSetRGB(0x00, 0x00, 0xff),
                               SkColorSetRGB(0xff, 0xff, 0xff)));
}

class HeadlessModeScreenshotCommandWithBackgroundBrowserTest
    : public HeadlessModeScreenshotCommandBrowserTest {
 public:
  HeadlessModeScreenshotCommandWithBackgroundBrowserTest() = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    HeadlessModeScreenshotCommandBrowserTest::SetUpCommandLine(command_line);

    command_line->AppendSwitchASCII(switches::kDefaultBackgroundColor,
                                    "ff0000");
  }
};

// TODO(crbug.com/517342172): Re-enable this test on Linux MSAN.
#if BUILDFLAG(IS_LINUX) && defined(MEMORY_SANITIZER)
#define MAYBE_HeadlessScreenshotWithBackground \
  DISABLED_HeadlessScreenshotWithBackground
#else
#define MAYBE_HeadlessScreenshotWithBackground HeadlessScreenshotWithBackground
#endif
IN_PROC_BROWSER_TEST_F(HeadlessModeScreenshotCommandWithBackgroundBrowserTest,
                       MAYBE_HeadlessScreenshotWithBackground) {
  ASSERT_THAT(ProcessCommands(),
              testing::Eq(HeadlessCommandHandler::Result::kSuccess));

  base::ScopedAllowBlockingForTesting allow_blocking;

  std::optional<std::vector<uint8_t>> png_data =
      base::ReadFileToBytes(screenshot_filename_);

  SkBitmap bitmap = gfx::PNGCodec::Decode(png_data.value());
  ASSERT_FALSE(bitmap.isNull());

  // Expect a centered blue rectangle on red background.
  EXPECT_TRUE(CheckColoredRect(bitmap, SkColorSetRGB(0x00, 0x00, 0xff),
                               SkColorSetRGB(0xff, 0x00, 0x00)));
}

// PrintToPDF command tests -------------------------------------------

class HeadlessModePrintToPdfCommandBrowserTestBase
    : public HeadlessModeCommandBrowserTestWithTempDir {
 public:
  HeadlessModePrintToPdfCommandBrowserTestBase() = default;

  void SetUpCommandLine(base::CommandLine* command_line) override {
    HeadlessModeCommandBrowserTestWithTempDir::SetUpCommandLine(command_line);

    print_to_pdf_filename_ =
        temp_dir().Append(FILE_PATH_LITERAL("print-to.pdf"));
    command_line->AppendSwitchPath(switches::kPrintToPDF,
                                   print_to_pdf_filename_);
    command_line->AppendSwitch(switches::kNoPDFHeaderFooter);

    command_line->AppendArg(GetTargetUrl(GetTargetPage()).spec());
  }

 protected:
  base::FilePath print_to_pdf_filename_;
};

class HeadlessModePrintToPdfCommandBrowserTest
    : public HeadlessModePrintToPdfCommandBrowserTestBase {
 public:
  HeadlessModePrintToPdfCommandBrowserTest() = default;

  std::string GetTargetPage() override { return "/centered_blue_box.html"; }
};

// TODO(crbug.com/40266323): Reenable once deflaked.
// TODO(crbug.com/514143472): Reenable once deflaked.
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_LINUX)
#define MAYBE_HeadlessPrintToPdf DISABLED_HeadlessPrintToPdf
#else
#define MAYBE_HeadlessPrintToPdf HeadlessPrintToPdf
#endif
IN_PROC_BROWSER_TEST_F(HeadlessModePrintToPdfCommandBrowserTest,
                       MAYBE_HeadlessPrintToPdf) {
  ASSERT_THAT(ProcessCommands(),
              testing::Eq(HeadlessCommandHandler::Result::kSuccess));

  base::ScopedAllowBlockingForTesting allow_blocking;

  std::optional<std::vector<uint8_t>> pdf_data =
      base::ReadFileToBytes(print_to_pdf_filename_);
  ASSERT_TRUE(pdf_data.has_value()) << print_to_pdf_filename_;

  PDFPageBitmap page_bitmap;
  ASSERT_TRUE(page_bitmap.Render(pdf_data.value(), /*page_index=*/0));

  // Expect blue rectangle on white background.
  EXPECT_TRUE(page_bitmap.CheckColoredRect(SkColorSetRGB(0x00, 0x00, 0xff),
                                           SkColorSetRGB(0xff, 0xff, 0xff)));
}

// TODO(crbug.com/517342172): Flaky on MSan Linux bots
#if defined(MEMORY_SANITIZER) && BUILDFLAG(IS_LINUX)
#define MAYBE_PrintToPdfWithLazyLoading DISABLED_PrintToPdfWithLazyLoading
#else
#define MAYBE_PrintToPdfWithLazyLoading PrintToPdfWithLazyLoading
#endif
HEADLESS_MODE_COMMAND_BROWSER_TEST_WITH_TARGET_URL(
    HeadlessModePrintToPdfCommandBrowserTestBase,
    MAYBE_PrintToPdfWithLazyLoading,
    "/page_with_lazy_image.html") {
  ASSERT_THAT(ProcessCommands(),
              testing::Eq(HeadlessCommandHandler::Result::kSuccess));

  base::ScopedAllowBlockingForTesting allow_blocking;

  std::optional<std::vector<uint8_t>> pdf_data =
      base::ReadFileToBytes(print_to_pdf_filename_);
  ASSERT_TRUE(pdf_data.has_value()) << print_to_pdf_filename_;

  PDFPageBitmap page_bitmap;
  ASSERT_TRUE(page_bitmap.Render(pdf_data.value(), /*page_index=*/4));

  // Expect green rectangle on white background.
  EXPECT_TRUE(page_bitmap.CheckColoredRect(SkColorSetRGB(0x00, 0x64, 0x00),
                                           SkColorSetRGB(0xff, 0xff, 0xff)));
}

class HeadlessModeTaggedPrintToPdfCommandBrowserTest
    : public HeadlessModePrintToPdfCommandBrowserTestBase,
      public ::testing::WithParamInterface<bool> {
 public:
  HeadlessModeTaggedPrintToPdfCommandBrowserTest() = default;

  bool generate_tagged_pdf() { return GetParam(); }

  std::string GetTargetPage() override { return "/hello.html"; }

  void SetUpCommandLine(base::CommandLine* command_line) override {
    HeadlessModePrintToPdfCommandBrowserTestBase::SetUpCommandLine(
        command_line);
    if (!generate_tagged_pdf()) {
      command_line->AppendSwitch(switches::kDisablePDFTagging);
    }
  }
};

const char kExpectedStructTreeJSON[] = R"({
   "lang": "en-US",
   "type": "Document",
   "~children": [ {
      "type": "H1",
      "~children": [ {
         "type": "NonStruct"
      } ]
   } ]
}
)";

INSTANTIATE_TEST_SUITE_P(/* no prefix */,
                         HeadlessModeTaggedPrintToPdfCommandBrowserTest,
                         ::testing::Bool());

// TODO(crbug.com/504964930): Reenable once deflaked.
// TODO(crbug.com/514143472): Reenable once deflaked.
#if BUILDFLAG(IS_LINUX) && \
    (defined(MEMORY_SANITIZER) || defined(USE_JAVASCRIPT_COVERAGE))
#define MAYBE_HeadlessTaggedPrintToPdf DISABLED_HeadlessTaggedPrintToPdf
#else
#define MAYBE_HeadlessTaggedPrintToPdf HeadlessTaggedPrintToPdf
#endif
IN_PROC_BROWSER_TEST_P(HeadlessModeTaggedPrintToPdfCommandBrowserTest,
                       MAYBE_HeadlessTaggedPrintToPdf) {
  ASSERT_THAT(ProcessCommands(),
              testing::Eq(HeadlessCommandHandler::Result::kSuccess));

  base::ScopedAllowBlockingForTesting allow_blocking;

  std::optional<std::vector<uint8_t>> pdf_data =
      base::ReadFileToBytes(print_to_pdf_filename_);
  ASSERT_TRUE(pdf_data.has_value()) << print_to_pdf_filename_;

  auto pdf_span = base::as_byte_span(pdf_data.value());

  int num_pages;
  ASSERT_TRUE(chrome_pdf::GetPDFDocInfo(pdf_span, &num_pages,
                                        /*max_page_width=*/nullptr));

  EXPECT_THAT(num_pages, testing::Eq(1));

  ASSERT_THAT(chrome_pdf::IsPDFDocTagged(pdf_span),
              testing::Optional(generate_tagged_pdf()));

  if (generate_tagged_pdf()) {
    base::Value struct_tree =
        chrome_pdf::GetPDFStructTreeForPage(pdf_span, /*page_index=*/0);
    EXPECT_THAT(kExpectedStructTreeJSON, base::test::IsJson((struct_tree)));
  }
}

class HeadlessModePrintToPdfPdfCommandBrowserTest
    : public HeadlessModePrintToPdfCommandBrowserTestBase {
 public:
  HeadlessModePrintToPdfPdfCommandBrowserTest() = default;

  void SetUp() override {
    embedded_test_server()->RegisterRequestHandler(base::BindRepeating(
        &HeadlessModePrintToPdfPdfCommandBrowserTest::RequestHandler,
        base::Unretained(this)));

    HeadlessModePrintToPdfCommandBrowserTestBase::SetUp();
  }

 private:
  std::string GetTargetPage() override { return "/test.pdf"; }

  std::unique_ptr<net::test_server::HttpResponse> RequestHandler(
      const net::test_server::HttpRequest& request) {
    if (request.relative_url == "/test.pdf") {
      auto response = std::make_unique<net::test_server::BasicHttpResponse>();
      response->set_code(net::HTTP_OK);
      response->set_content_type("application/pdf");
      response->set_content("%PDF-1.4");
      return response;
    }

    return nullptr;
  }
};

IN_PROC_BROWSER_TEST_F(HeadlessModePrintToPdfPdfCommandBrowserTest,
                       PrintToPdfPdf) {
  // Navigation to a PDF file is handled by the PDF viewer extension in
  // headless mode (unlike headless shell where it is aborted).
  EXPECT_THAT(ProcessCommands(),
              testing::Eq(HeadlessCommandHandler::Result::kSuccess));

  base::ScopedAllowBlockingForTesting allow_blocking;
  EXPECT_TRUE(base::PathExists(print_to_pdf_filename_));
}

}  // namespace headless
