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

#include "chrome/browser/printing/pdf_nup_converter_client.h"

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

#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "base/files/file_util.h"
#include "base/functional/bind.h"
#include "base/notreached.h"
#include "base/path_service.h"
#include "base/test/test_future.h"
#include "base/threading/thread_restrictions.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "content/public/test/browser_test.h"
#include "pdf/pdf.h"
#include "printing/print_settings.h"
#include "ui/gfx/geometry/size_f.h"

namespace printing {

namespace {

base::FilePath GetTestDir() {
  base::FilePath dir;
  base::PathService::Get(chrome::DIR_TEST_DATA, &dir);
  if (!dir.empty())
    dir = dir.AppendASCII("printing");
  return dir;
}

base::MappedReadOnlyRegion GetPdfRegion(const char* file_name) {
  base::ScopedAllowBlockingForTesting allow_blocking;
  base::MappedReadOnlyRegion pdf_region;

  base::FilePath test_data_dir = GetTestDir();
  if (test_data_dir.empty())
    return pdf_region;

  base::FilePath pdf_file = test_data_dir.AppendASCII(file_name);
  std::string pdf_str;
  if (!base::ReadFileToString(pdf_file, &pdf_str) || pdf_str.empty())
    return pdf_region;

  pdf_region = base::ReadOnlySharedMemoryRegion::Create(pdf_str.size());
  if (!pdf_region.IsValid())
    return pdf_region;

  UNSAFE_TODO(
      memcpy(pdf_region.mapping.memory(), pdf_str.data(), pdf_str.size()));
  return pdf_region;
}

base::MappedReadOnlyRegion GetBadDataRegion() {
  static const char kBadData[] = "BADDATA";
  base::MappedReadOnlyRegion pdf_region =
      base::ReadOnlySharedMemoryRegion::Create(std::size(kBadData));
  if (!pdf_region.IsValid())
    return pdf_region;

  UNSAFE_TODO(
      memcpy(pdf_region.mapping.memory(), kBadData, std::size(kBadData)));
  return pdf_region;
}

std::vector<gfx::SizeF> GetPdfPageSizes(base::span<const uint8_t> pdf_data) {
  int num_pages;
  if (!chrome_pdf::GetPDFDocInfo(pdf_data, &num_pages, nullptr) ||
      num_pages <= 0) {
    return {};
  }

  std::vector<gfx::SizeF> sizes;
  for (int i = 0; i < num_pages; ++i) {
    std::optional<gfx::SizeF> page_size =
        chrome_pdf::GetPDFPageSizeByIndex(pdf_data, i);
    if (!page_size.has_value())
      return {};

    sizes.push_back(page_size.value());
  }

  return sizes;
}

void VerifyPdf(base::span<const uint8_t> pdf_data,
               base::span<const gfx::SizeF> expected_sizes) {
  std::vector<gfx::SizeF> page_sizes = GetPdfPageSizes(pdf_data);
  ASSERT_EQ(expected_sizes.size(), page_sizes.size());
  for (size_t i = 0; i < expected_sizes.size(); ++i)
    EXPECT_EQ(expected_sizes[i], page_sizes[i]);
}

base::span<const gfx::SizeF> GetExpectedPdfSizes(std::string_view pdf_name) {
  if (pdf_name == "hello_world.pdf") {
    static constexpr gfx::SizeF kSizes[] = {
        {612.0f, 792.0f},
    };
    return kSizes;
  }
  if (pdf_name == "pdf_converter_basic.pdf") {
    static constexpr gfx::SizeF kSizes[] = {
        {612.0f, 792.0f},
        {612.0f, 792.0f},
        {612.0f, 792.0f},
    };
    return kSizes;
  }

  NOTREACHED();
}

}  // namespace

class PdfNupConverterClientBrowserTest : public InProcessBrowserTest {
 public:
  struct ConvertResult {
    mojom::PdfNupConverter::Status status;
    base::ReadOnlySharedMemoryRegion nup_pdf_region;
  };

  using NupTestFuture =
      base::test::TestFuture<mojom::PdfNupConverter::Status,
                             base::ReadOnlySharedMemoryRegion>;

  PdfNupConverterClientBrowserTest() = default;
  ~PdfNupConverterClientBrowserTest() override = default;

  std::optional<ConvertResult> ConvertDocument(
      base::ReadOnlySharedMemoryRegion pdf_region,
      int pages_per_sheet) {
    auto converter = std::make_unique<PdfNupConverterClient>(
        browser()->tab_strip_model()->GetActiveWebContents());

    NupTestFuture future;
    converter->DoNupPdfDocumentConvert(
        /*document_cookie=*/PrintSettings::NewCookie(), pages_per_sheet,
        /*page_size=*/gfx::Size(612, 792),
        /*printable_area=*/gfx::Rect(612, 792), std::move(pdf_region),
        future.GetCallback());

    if (!future.Wait()) {
      // Give the caller a chance to fail gracefully. Whereas calling
      // `future.Take()` without handling the Wait() failure will result in a
      // CHECK() crash.
      return std::nullopt;
    }

    auto result = future.Take();
    return ConvertResult{std::get<0>(result), std::move(std::get<1>(result))};
  }

  std::optional<ConvertResult> ConvertPages(
      std::vector<base::ReadOnlySharedMemoryRegion> pdf_regions,
      int pages_per_sheet) {
    auto converter = std::make_unique<PdfNupConverterClient>(
        browser()->tab_strip_model()->GetActiveWebContents());

    NupTestFuture future;
    converter->DoNupPdfConvert(
        /*document_cookie=*/PrintSettings::NewCookie(), pages_per_sheet,
        /*page_size=*/gfx::Size(612, 792),
        /*printable_area=*/gfx::Rect(612, 792), std::move(pdf_regions),
        future.GetCallback());

    if (!future.Wait()) {
      // See comment in the `future.Wait()` call in ConvertDocument() above.
      return std::nullopt;
    }

    auto result = future.Take();
    return ConvertResult{std::get<0>(result), std::move(std::get<1>(result))};
  }
};

IN_PROC_BROWSER_TEST_F(PdfNupConverterClientBrowserTest,
                       DocumentConvert2UpSuccess) {
  base::MappedReadOnlyRegion pdf_region =
      GetPdfRegion("pdf_converter_basic.pdf");
  ASSERT_TRUE(pdf_region.IsValid());

  // Make sure pdf_converter_basic.pdf is as expected.
  VerifyPdf(pdf_region.mapping.GetMemoryAsSpan<uint8_t>(),
            GetExpectedPdfSizes("pdf_converter_basic.pdf"));

  std::optional<ConvertResult> result =
      ConvertDocument(std::move(pdf_region.region), /*pages_per_sheet=*/2);

  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(mojom::PdfNupConverter::Status::SUCCESS, result.value().status);
  base::ReadOnlySharedMemoryMapping nup_pdf_mapping =
      result.value().nup_pdf_region.Map();
  ASSERT_TRUE(nup_pdf_mapping.IsValid());

  // For 2-up, a 3 page portrait document fits on 2 landscape-oriented pages.
  static constexpr gfx::SizeF kExpectedSizes[] = {
      {792.0f, 612.0f},
      {792.0f, 612.0f},
  };
  VerifyPdf(nup_pdf_mapping.GetMemoryAsSpan<uint8_t>(), kExpectedSizes);
}

IN_PROC_BROWSER_TEST_F(PdfNupConverterClientBrowserTest,
                       DocumentConvert4UpSuccess) {
  base::MappedReadOnlyRegion pdf_region =
      GetPdfRegion("pdf_converter_basic.pdf");
  ASSERT_TRUE(pdf_region.IsValid());

  // Make sure pdf_converter_basic.pdf is as expected.
  VerifyPdf(pdf_region.mapping.GetMemoryAsSpan<uint8_t>(),
            GetExpectedPdfSizes("pdf_converter_basic.pdf"));

  std::optional<ConvertResult> result =
      ConvertDocument(std::move(pdf_region.region), /*pages_per_sheet=*/4);

  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(mojom::PdfNupConverter::Status::SUCCESS, result.value().status);
  base::ReadOnlySharedMemoryMapping nup_pdf_mapping =
      result.value().nup_pdf_region.Map();
  ASSERT_TRUE(nup_pdf_mapping.IsValid());

  // For 4-up, a 3 page portrait document fits on 1 portrait-oriented page.
  static constexpr gfx::SizeF kExpectedSizes[] = {
      {612.0f, 792.0f},
  };
  VerifyPdf(nup_pdf_mapping.GetMemoryAsSpan<uint8_t>(), kExpectedSizes);
}

IN_PROC_BROWSER_TEST_F(PdfNupConverterClientBrowserTest,
                       DocumentConvertBadData) {
  base::MappedReadOnlyRegion pdf_region = GetBadDataRegion();
  ASSERT_TRUE(pdf_region.IsValid());

  std::optional<ConvertResult> result =
      ConvertDocument(std::move(pdf_region.region), /*pages_per_sheet=*/2);

  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(mojom::PdfNupConverter::Status::CONVERSION_FAILURE,
            result.value().status);
  base::ReadOnlySharedMemoryMapping nup_pdf_mapping =
      result.value().nup_pdf_region.Map();
  EXPECT_FALSE(nup_pdf_mapping.IsValid());
}

IN_PROC_BROWSER_TEST_F(PdfNupConverterClientBrowserTest,
                       PagesConvert2UpSuccess) {
  std::vector<base::ReadOnlySharedMemoryRegion> pdf_regions;
  {
    base::MappedReadOnlyRegion pdf_region = GetPdfRegion("hello_world.pdf");
    ASSERT_TRUE(pdf_region.IsValid());

    // Make sure hello_world.pdf is as expected.
    VerifyPdf(pdf_region.mapping.GetMemoryAsSpan<uint8_t>(),
              GetExpectedPdfSizes("hello_world.pdf"));

    // Use hello_world.pdf, which only has 1 page, as the 2 pages of input for
    // this N-up operation.
    auto pdf_region_copy = pdf_region.region.Duplicate();
    ASSERT_TRUE(pdf_region_copy.IsValid());
    pdf_regions.push_back(std::move(pdf_region_copy));
    pdf_regions.push_back(std::move(pdf_region.region));
  }

  std::optional<ConvertResult> result =
      ConvertPages(std::move(pdf_regions), /*pages_per_sheet=*/2);

  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(mojom::PdfNupConverter::Status::SUCCESS, result.value().status);
  base::ReadOnlySharedMemoryMapping nup_pdf_mapping =
      result.value().nup_pdf_region.Map();
  ASSERT_TRUE(nup_pdf_mapping.IsValid());

  // For 2-up, a 2 page portrait document fits on 1 landscape-oriented pages.
  static constexpr gfx::SizeF kExpectedSizes[] = {
      {792.0f, 612.0f},
  };
  VerifyPdf(nup_pdf_mapping.GetMemoryAsSpan<uint8_t>(), kExpectedSizes);
}

IN_PROC_BROWSER_TEST_F(PdfNupConverterClientBrowserTest, PagesConvertBadData) {
  std::vector<base::ReadOnlySharedMemoryRegion> pdf_regions;
  {
    base::MappedReadOnlyRegion pdf_region = GetPdfRegion("hello_world.pdf");
    ASSERT_TRUE(pdf_region.IsValid());

    // Make sure hello_world.pdf is as expected.
    VerifyPdf(pdf_region.mapping.GetMemoryAsSpan<uint8_t>(),
              GetExpectedPdfSizes("hello_world.pdf"));

    base::MappedReadOnlyRegion bad_pdf_region = GetBadDataRegion();
    ASSERT_TRUE(bad_pdf_region.IsValid());

    pdf_regions.push_back(std::move(pdf_region.region));
    pdf_regions.push_back(std::move(bad_pdf_region.region));
  }

  std::optional<ConvertResult> result =
      ConvertPages(std::move(pdf_regions), /*pages_per_sheet=*/2);

  ASSERT_TRUE(result.has_value());
  EXPECT_EQ(mojom::PdfNupConverter::Status::CONVERSION_FAILURE,
            result.value().status);
  base::ReadOnlySharedMemoryMapping nup_pdf_mapping =
      result.value().nup_pdf_region.Map();
  EXPECT_FALSE(nup_pdf_mapping.IsValid());
}

}  // namespace printing
