// 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 <memory>
#include <tuple>
#include <utility>
#include <variant>
#include <vector>

#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/weak_ptr.h"
#include "base/run_loop.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/pdf/pdf_extension_test_base.h"
#include "chrome/browser/pdf/pdf_extension_test_util.h"
#include "chrome/browser/printing/browser_printing_context_factory_for_test.h"
#include "chrome/browser/printing/print_error_dialog.h"
#include "chrome/browser/printing/print_job.h"
#include "chrome/browser/printing/print_test_utils.h"
#include "chrome/browser/printing/print_view_manager_base.h"
#include "chrome/browser/printing/test_print_preview_observer.h"
#include "chrome/browser/printing/test_print_view_manager.h"
#include "chrome/browser/renderer_context_menu/render_view_context_menu_browsertest_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/common/chrome_switches.h"
#include "components/keyed_service/content/browser_context_dependency_manager.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/render_widget_host.h"
#include "content/public/browser/web_contents.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/context_menu_interceptor.h"
#include "extensions/browser/guest_view/mime_handler_view/mime_handler_view_guest.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "pdf/pdf_features.h"
#include "printing/backend/print_backend.h"
#include "printing/backend/test_print_backend.h"
#include "printing/buildflags/buildflags.h"
#include "printing/printing_features.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/input/web_input_event.h"
#include "third_party/blink/public/common/input/web_mouse_event.h"
#include "ui/base/mojom/menu_source_type.mojom.h"

#if BUILDFLAG(ENABLE_OOP_PRINTING)
#include "chrome/browser/printing/print_backend_service_manager.h"
#include "chrome/browser/printing/print_backend_service_test_impl.h"
#endif

#if BUILDFLAG(ENABLE_PRINT_PREVIEW)
#include "ui/base/ui_base_types.h"
#endif

#if BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ash/printing/cups_print_job_manager_factory.h"
#include "chrome/browser/ash/printing/cups_printers_manager_factory.h"
#include "chrome/browser/ash/printing/fake_cups_printers_manager.h"
#include "chrome/browser/ash/printing/test_cups_print_job_manager.h"
#endif

namespace {

struct PDFExtensionPrintingTestPassToString {
  std::string operator()(
      const ::testing::TestParamInfo<std::tuple<bool, bool>>& i) const {
    return std::string(std::get<1>(i.param) ? "OOPIF_" : "GUESTVIEW_") +
           std::string(std::get<0>(i.param) ? "SERVICE" : "BROWSER");
  }
};

#if BUILDFLAG(IS_CHROMEOS)
std::unique_ptr<KeyedService> BuildTestCupsPrintJobManager(
    content::BrowserContext* context) {
  return std::make_unique<ash::TestCupsPrintJobManager>(
      Profile::FromBrowserContext(context));
}

std::unique_ptr<KeyedService> BuildFakeCupsPrintersManager(
    content::BrowserContext* context) {
  return std::make_unique<ash::FakeCupsPrintersManager>();
}

void OnWillCreateBrowserContextServices(content::BrowserContext* context) {
  ash::CupsPrintJobManagerFactory::GetInstance()->SetTestingFactory(
      context, base::BindRepeating(&BuildTestCupsPrintJobManager));
  ash::CupsPrintersManagerFactory::GetInstance()->SetTestingFactory(
      context, base::BindRepeating(&BuildFakeCupsPrintersManager));
}
#endif

}  // namespace

using ::content::WebContents;
using ::extensions::MimeHandlerViewGuest;
using ::pdf_extension_test_util::SetInputFocusOnPlugin;

class PDFExtensionPrintingTest
    : public PDFExtensionTestBase,
      public printing::PrintJob::Observer,
      public testing::WithParamInterface<std::tuple<bool, bool>> {
 public:
  PDFExtensionPrintingTest() = default;
  ~PDFExtensionPrintingTest() override = default;

  // PDFExtensionTestBase:
  void SetUp() override {
    // Avoid using a real PrintBackend / PrintingContext, as they can show modal
    // print dialogs.
    // Called here in SetUp() because it must be reset in TearDown(), as
    // resetting in TearDownOnMainThread() is too early. The MessagePump may
    // still process messages after TearDownOnMainThread(), which can trigger
    // PrintingContext calls.
    printing::PrintBackend::SetPrintBackendForTesting(
        test_print_backend_.get());
    printing::PrintingContext::SetPrintingContextFactoryForTest(
        &test_printing_context_factory_);

    // Tests assume that printing can be done to the print backend, so there
    // must be at least one printer available for that to make sense.
    constexpr char kTestPrinter[] = "printer1";
    AddPrinter(kTestPrinter);
    test_printing_context_factory_.SetPrinterNameForSubsequentContexts(
        kTestPrinter);

    PDFExtensionTestBase::SetUp();
  }
  void SetUpOnMainThread() override {
    // Avoid getting blocked by modal print error dialogs. Must be called after
    // the UI thread is up and running.
    SetShowPrintErrorDialogForTest(base::DoNothing());
#if BUILDFLAG(ENABLE_OOP_PRINTING)
    if (UseService()) {
      print_backend_service_ =
          printing::PrintBackendServiceTestImpl::LaunchForTesting(
              test_remote_, test_print_backend_.get(), /*sandboxed=*/true);
    }
#endif
    PDFExtensionTestBase::SetUpOnMainThread();
  }
#if BUILDFLAG(IS_CHROMEOS)
  void SetUpInProcessBrowserTestFixture() override {
    create_services_subscription_ =
        BrowserContextDependencyManager::GetInstance()
            ->RegisterCreateServicesCallbackForTesting(
                base::BindRepeating(&OnWillCreateBrowserContextServices));
  }
#endif

  void TearDownOnMainThread() override {
    PDFExtensionTestBase::TearDownOnMainThread();
#if BUILDFLAG(ENABLE_OOP_PRINTING)
    printing::PrintBackendServiceManager::ResetForTesting();
#endif
    SetShowPrintErrorDialogForTest(base::NullCallback());
  }
  void TearDown() override {
    PDFExtensionTestBase::TearDown();
    printing::PrintingContext::SetPrintingContextFactoryForTest(nullptr);
    printing::PrintBackend::SetPrintBackendForTesting(nullptr);
  }
  bool UseOopif() const override { return std::get<1>(GetParam()); }
  std::vector<base::test::FeatureRefAndParams> GetEnabledFeatures()
      const override {
    std::vector<base::test::FeatureRefAndParams> enabled =
        PDFExtensionTestBase::GetEnabledFeatures();
#if BUILDFLAG(ENABLE_OOP_PRINTING)
    if (UseService()) {
      enabled.push_back({printing::features::kEnableOopPrintDrivers, {}});
    }
#endif
    return enabled;
  }
  std::vector<base::test::FeatureRef> GetDisabledFeatures() const override {
    std::vector<base::test::FeatureRef> disabled =
        PDFExtensionTestBase::GetDisabledFeatures();
#if BUILDFLAG(ENABLE_OOP_PRINTING)
    if (!UseService()) {
      disabled.push_back(printing::features::kEnableOopPrintDrivers);
    }
#endif
    return disabled;
  }

  void AddPrinter(const std::string& printer_name) {
    printing::PrinterBasicInfo printer_info(
        printer_name,
        /*display_name=*/"test printer",
        /*printer_description=*/"A printer for testing.",
        printing::test::GetPrintInfoOptions());

    auto default_caps =
        std::make_unique<printing::PrinterSemanticCapsAndDefaults>();
    default_caps->copies_max = 1;
    default_caps->dpis = printing::test::GetPrinterCapabilitiesDefaultDpis();
    default_caps->default_dpi = printing::test::kPrinterCapabilitiesDpi;
    default_caps->papers.push_back(printing::test::GetPaperLetter());
    default_caps->papers.push_back(printing::test::GetPaperLegal());
    test_print_backend_->AddValidPrinter(
        printer_name, std::move(default_caps),
        std::make_unique<printing::PrinterBasicInfo>(printer_info));
    test_print_backend_->SetDefaultPrinterName(printer_name);
  }

  void SetupPrintViewManagerForJobMonitoring(content::RenderFrameHost* frame) {
    auto* web_contents = content::WebContents::FromRenderFrameHost(frame);
    auto manager = std::make_unique<printing::TestPrintViewManager>(
        web_contents,
        base::BindRepeating(&PDFExtensionPrintingTest::OnCreatedPrintJob,
                            weak_factory_.GetWeakPtr()));
    web_contents->SetUserData(printing::PrintViewManager::UserDataKey(),
                              std::move(manager));
  }

  void WaitForPrintJobDestruction() {
    if (!print_job_destroyed_) {
      base::RunLoop run_loop;
      base::AutoReset<raw_ptr<base::RunLoop>> auto_reset(&run_loop_, &run_loop);
      run_loop.Run();
      EXPECT_TRUE(print_job_destroyed_);
    }
  }

 private:
  bool UseService() const { return std::get<0>(GetParam()); }

  void OnCreatedPrintJob(printing::PrintJob* print_job) {
    EXPECT_FALSE(observing_print_job_);
    print_job->AddObserver(*this);
    observing_print_job_ = true;
  }

  // PrintJob::Observer:
  void OnDestruction() override {
    EXPECT_FALSE(print_job_destroyed_);
    if (run_loop_) {
      run_loop_->Quit();
    }
    print_job_destroyed_ = true;
  }

#if BUILDFLAG(IS_CHROMEOS)
  base::CallbackListSubscription create_services_subscription_;
#endif

  scoped_refptr<printing::TestPrintBackend> test_print_backend_ =
      base::MakeRefCounted<printing::TestPrintBackend>();
  printing::BrowserPrintingContextFactoryForTest test_printing_context_factory_;
#if BUILDFLAG(ENABLE_OOP_PRINTING)
  mojo::Remote<printing::mojom::PrintBackendService> test_remote_;
  std::unique_ptr<printing::PrintBackendServiceTestImpl> print_backend_service_;
#endif
  bool observing_print_job_ = false;
  bool print_job_destroyed_ = false;
  raw_ptr<base::RunLoop> run_loop_ = nullptr;
  base::WeakPtrFactory<PDFExtensionPrintingTest> weak_factory_{this};
};

IN_PROC_BROWSER_TEST_P(PDFExtensionPrintingTest, BasicPrintCommand) {
  ASSERT_TRUE(LoadPdf(embedded_test_server()->GetURL("/pdf/test.pdf")));
  content::RenderFrameHost* plugin_frame =
      pdf_extension_test_util::GetOnlyPdfPluginFrame(GetActiveWebContents());
  ASSERT_TRUE(plugin_frame);

  SetupPrintViewManagerForJobMonitoring(plugin_frame);
  chrome::BasicPrint(browser());
  WaitForPrintJobDestruction();
}

#if BUILDFLAG(ENABLE_PRINT_PREVIEW)
IN_PROC_BROWSER_TEST_P(PDFExtensionPrintingTest, PrintCommand) {
  ASSERT_TRUE(LoadPdf(embedded_test_server()->GetURL("/pdf/test.pdf")));

  printing::TestPrintPreviewObserver print_observer(/*wait_for_loaded=*/false);
  chrome::Print(browser());
  print_observer.WaitUntilPreviewIsReady();
}

IN_PROC_BROWSER_TEST_P(PDFExtensionPrintingTest,
                       ContextMenuPrintCommandExtensionMainFrame) {
  content::RenderFrameHost* extension_host =
      LoadPdfGetExtensionHost(embedded_test_server()->GetURL("/pdf/test.pdf"));
  ASSERT_TRUE(extension_host);

  content::WebContents* embedder_web_contents = GetEmbedderWebContents();
  ASSERT_TRUE(embedder_web_contents);

  // Makes sure that the correct frame invoked the context menu.
  content::ContextMenuInterceptor menu_interceptor(extension_host);

  // Executes the print command as soon as the context menu is shown.
  ContextMenuNotificationObserver context_menu_observer(IDC_PRINT);

  printing::TestPrintPreviewObserver print_observer(/*wait_for_loaded=*/false);
  SimulateMouseClickAt(extension_host, embedder_web_contents,
                       blink::WebInputEvent::kNoModifiers,
                       blink::WebMouseEvent::Button::kRight, {1, 1});
  print_observer.WaitUntilPreviewIsReady();
  menu_interceptor.Wait();
}

IN_PROC_BROWSER_TEST_P(PDFExtensionPrintingTest,
                       ContextMenuPrintCommandEmbeddedExtensionMainFrame) {
  content::RenderFrameHost* extension_host =
      LoadPdfInFirstChildGetExtensionHost(
          embedded_test_server()->GetURL("/pdf/pdf_embed.html"));
  ASSERT_TRUE(extension_host);

  content::WebContents* embedder_web_contents = GetEmbedderWebContents();
  ASSERT_TRUE(embedder_web_contents);

  // Makes sure that the correct frame invoked the context menu.
  content::ContextMenuInterceptor menu_interceptor(extension_host);

  // Executes the print command as soon as the context menu is shown.
  ContextMenuNotificationObserver context_menu_observer(IDC_PRINT);

  printing::TestPrintPreviewObserver print_observer(/*wait_for_loaded=*/false);
  SimulateMouseClickAt(extension_host, embedder_web_contents,
                       blink::WebInputEvent::kNoModifiers,
                       blink::WebMouseEvent::Button::kRight, {1, 1});
  print_observer.WaitUntilPreviewIsReady();
  menu_interceptor.Wait();
}

IN_PROC_BROWSER_TEST_P(PDFExtensionPrintingTest,
                       ContextMenuPrintCommandPluginFrame) {
  content::RenderFrameHost* extension_host =
      LoadPdfGetExtensionHost(embedded_test_server()->GetURL("/pdf/test.pdf"));
  ASSERT_TRUE(extension_host);

  content::RenderFrameHost* plugin_frame =
      pdf_extension_test_util::GetOnlyPdfPluginFrame(GetActiveWebContents());
  ASSERT_TRUE(plugin_frame);

  content::WebContents* embedder_web_contents = GetEmbedderWebContents();
  ASSERT_TRUE(embedder_web_contents);

  // Makes sure that the correct frame invoked the context menu.
  content::ContextMenuInterceptor menu_interceptor(plugin_frame);

  // Executes the print command as soon as the context menu is shown.
  ContextMenuNotificationObserver context_menu_observer(IDC_PRINT);

  printing::TestPrintPreviewObserver print_observer(/*wait_for_loaded=*/false);
  SetInputFocusOnPlugin(extension_host, embedder_web_contents);
  plugin_frame->GetRenderWidgetHost()->ShowContextMenuAtPoint(
      {1, 1}, ui::mojom::MenuSourceType::kMouse);
  print_observer.WaitUntilPreviewIsReady();
  menu_interceptor.Wait();
}

// TODO(crbug.com/40842943): Fix flakiness.
IN_PROC_BROWSER_TEST_P(PDFExtensionPrintingTest,
                       DISABLED_ContextMenuPrintCommandEmbeddedPluginFrame) {
  content::RenderFrameHost* extension_host =
      LoadPdfInFirstChildGetExtensionHost(
          embedded_test_server()->GetURL("/pdf/pdf_embed.html"));
  ASSERT_TRUE(extension_host);

  content::RenderFrameHost* plugin_frame =
      pdf_extension_test_util::GetOnlyPdfPluginFrame(GetActiveWebContents());
  ASSERT_TRUE(plugin_frame);

  content::WebContents* embedder_web_contents = GetEmbedderWebContents();
  ASSERT_TRUE(embedder_web_contents);

  // Makes sure that the correct frame invoked the context menu.
  content::ContextMenuInterceptor menu_interceptor(plugin_frame);

  // Executes the print command as soon as the context menu is shown.
  ContextMenuNotificationObserver context_menu_observer(IDC_PRINT);

  printing::TestPrintPreviewObserver print_observer(/*wait_for_loaded=*/false);
  SetInputFocusOnPlugin(extension_host, embedder_web_contents);
  plugin_frame->GetRenderWidgetHost()->ShowContextMenuAtPoint(
      {1, 1}, ui::mojom::MenuSourceType::kMouse);
  print_observer.WaitUntilPreviewIsReady();
  menu_interceptor.Wait();
}

IN_PROC_BROWSER_TEST_P(PDFExtensionPrintingTest, PrintButton) {
  content::RenderFrameHost* extension_host =
      LoadPdfGetExtensionHost(embedded_test_server()->GetURL("/pdf/test.pdf"));
  ASSERT_TRUE(extension_host);

  printing::TestPrintPreviewObserver print_observer(/*wait_for_loaded=*/false);
  constexpr char kClickPrintButtonScript[] = R"(
    viewer.shadowRoot.querySelector('#toolbar')
        .shadowRoot.querySelector('#print')
        .click();
  )";
  EXPECT_TRUE(ExecJs(extension_host, kClickPrintButtonScript));
  print_observer.WaitUntilPreviewIsReady();
}
#endif  // BUILDFLAG(ENABLE_PRINT_PREVIEW)

INSTANTIATE_TEST_SUITE_P(All,
                         PDFExtensionPrintingTest,
                         testing::Combine(
#if BUILDFLAG(ENABLE_OOP_PRINTING)
                             testing::Bool(),
#else
                             testing::Values(false),
#endif
                             testing::Bool()),
                         PDFExtensionPrintingTestPassToString());

class PDFExtensionBasicPrintingTest : public PDFExtensionPrintingTest {
 public:
  PDFExtensionBasicPrintingTest() = default;
  ~PDFExtensionBasicPrintingTest() override = default;

  // PDFExtensionPrintingTest:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    command_line->AppendSwitch(switches::kDisablePrintPreview);
    PDFExtensionPrintingTest::SetUpCommandLine(command_line);
  }
};

// TODO(crbug.com/40283511): Test is flaky.
#if BUILDFLAG(IS_MAC)
#define MAYBE_ContextMenuPrintCommandExtensionMainFrame \
  DISABLED_ContextMenuPrintCommandExtensionMainFrame
#else
#define MAYBE_ContextMenuPrintCommandExtensionMainFrame \
  ContextMenuPrintCommandExtensionMainFrame
#endif
IN_PROC_BROWSER_TEST_P(PDFExtensionBasicPrintingTest,
                       MAYBE_ContextMenuPrintCommandExtensionMainFrame) {
  content::RenderFrameHost* extension_host =
      LoadPdfGetExtensionHost(embedded_test_server()->GetURL("/pdf/test.pdf"));
  ASSERT_TRUE(extension_host);

  content::RenderFrameHost* plugin_frame =
      pdf_extension_test_util::GetOnlyPdfPluginFrame(GetActiveWebContents());
  ASSERT_TRUE(plugin_frame);

  content::WebContents* embedder_web_contents = GetEmbedderWebContents();
  ASSERT_TRUE(embedder_web_contents);

  // Makes sure that the correct frame invoked the context menu.
  content::ContextMenuInterceptor menu_interceptor(plugin_frame);

  // Executes the print command as soon as the context menu is shown.
  ContextMenuNotificationObserver context_menu_observer(IDC_PRINT);

  SetupPrintViewManagerForJobMonitoring(plugin_frame);
  SetInputFocusOnPlugin(extension_host, embedder_web_contents);
  SimulateMouseClickAt(plugin_frame, embedder_web_contents,
                       blink::WebInputEvent::kNoModifiers,
                       blink::WebMouseEvent::Button::kRight, {1, 1});
  menu_interceptor.Wait();
  WaitForPrintJobDestruction();
}

INSTANTIATE_TEST_SUITE_P(All,
                         PDFExtensionBasicPrintingTest,
                         testing::Combine(testing::Bool(), testing::Bool()),
                         PDFExtensionPrintingTestPassToString());
