// 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 <cups/ipp.h>

#include "base/functional/callback_helpers.h"
#include "base/test/gmock_callback_support.h"
#include "base/test/values_test_util.h"
#include "chrome/browser/chromeos/printing/cups_wrapper.h"
#include "chrome/browser/content_settings/host_content_settings_map_factory.h"
#include "chrome/browser/extensions/api/printing/printing_test_utils.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/web_applications/test/isolated_web_app_test_utils.h"
#include "chrome/browser/web_applications/isolated_web_apps/isolated_web_app_url_info.h"
#include "chrome/browser/web_applications/isolated_web_apps/test/isolated_web_app_builder.h"
#include "chromeos/printing/printer_configuration.h"
#include "components/content_settings/core/browser/host_content_settings_map.h"
#include "components/content_settings/core/common/content_settings_types.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "printing/backend/cups_ipp_constants.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/common/features_generated.h"

#if BUILDFLAG(IS_CHROMEOS)
#include "base/test/mock_callback.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/printing/print_job.h"
#include "chrome/browser/printing/print_job_manager.h"
#include "printing/print_settings.h"
#include "printing/printed_document.h"
#endif

namespace printing {

namespace {

constexpr char kId[] = "id";
constexpr char kName[] = "name";

constexpr std::string_view kPrintScriptWithJobStatePlaceholder = R"(
    (async () => {
      const pdf = `%PDF-1.0
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 ` +
`obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 ` +
`obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
xref
0 4
0000000000 65535 f
0000000010 00000 n
0000000053 00000 n
0000000102 00000 n
trailer<</Size 4/Root 1 0 R>>
startxref
149
%EOF`;

    const pdfBlob = new Blob([pdf], {type: 'application/pdf'});
    const printers = await printing.getPrinters();

    const printJob = await printers[0].submitPrintJob("Title",
      pdfBlob,
      {
        mediaCol: {
          mediaSize: {
            xDimension: 21000,
            yDimension: 29700,
          }
        },
        mediaSource: "tray-1",
        printColorMode: "color",
        multipleDocumentHandling: "separate-documents-collated-copies",
        printerResolution: {
          crossFeedDirectionResolution: 300,
          feedDirectionResolution: 400,
          units: "dots-per-inch",
        },
      });
    const printJobComplete = new Promise((resolve, reject) => {
      printJob.onjobstatechange = () => {
        if (printJob.attributes().jobState === $1) {
          resolve();
          return;
        }
      };
    });
    await printJobComplete;
   })();
  )";

using testing::_;
using testing::AtMost;

#if BUILDFLAG(IS_CHROMEOS)
using testing::AllOf;
using testing::Contains;
using testing::Eq;
using testing::Field;
using testing::Pair;
using testing::Pointee;
using testing::Property;
#endif

class MockCupsWrapper : public chromeos::CupsWrapper {
 public:
  ~MockCupsWrapper() override = default;

  MOCK_METHOD(void,
              QueryCupsPrintJobs,
              (const std::vector<std::string>& printer_ids,
               base::OnceCallback<void(std::unique_ptr<QueryResult>)> callback),
              (override));
  MOCK_METHOD(void,
              CancelJob,
              (const std::string& printer_id, int job_id),
              (override));
  MOCK_METHOD(
      void,
      QueryCupsPrinterStatus,
      (const std::string& printer_id,
       base::OnceCallback<void(std::unique_ptr<::printing::PrinterStatus>)>
           callback),
      (override));
};

#if BUILDFLAG(IS_CHROMEOS)
auto ValidatePrintSettings() {
  // These are synced with the `kPrintScriptWithJobStatePlaceholder` script.
  return AllOf(
      // copies:
      Property(&PrintSettings::copies, Eq(1)),
      // mediaCol:
      Property(&PrintSettings::requested_media,
               Field(&PrintSettings::RequestedMedia::size_microns,
                     Eq(gfx::Size(210000, 297000)))),
      // mediaSource:
      Property(&PrintSettings::advanced_settings,
               Contains(Pair(Eq(printing::kIppMediaSource),
                             Property(&base::Value::GetIfString,
                                      Pointee(Eq("tray-1")))))),
      // printColorMode:
      Property(&PrintSettings::color, Eq(mojom::ColorModel::kColorModeColor)),
      // printQuality:
      Property(&PrintSettings::quality, Eq(mojom::Quality::kUnknownQuality)),
      Property(&PrintSettings::title, Eq(u"Title")),
      // multipleDocumentHandling:
      Property(&PrintSettings::collate, Eq(true)),
      // printerResolution:
      Property(&PrintSettings::dpi_size, Eq(gfx::Size(300, 400))));
}
#endif

}  // namespace

class WebPrintingBrowserTestBase
    : public web_app::IsolatedWebAppBrowserTestHarness {
 public:
  void SetUpOnMainThread() override {
    IsolatedWebAppBrowserTestHarness::SetUpOnMainThread();
    std::unique_ptr<web_app::ScopedBundledIsolatedWebApp> app =
        web_app::IsolatedWebAppBuilder(
            web_app::ManifestBuilder().AddPermissionsPolicyWildcard(
                network::mojom::PermissionsPolicyFeature::kWebPrinting))
            .BuildBundle();
    app->TrustSigningKey();
    web_app::IsolatedWebAppUrlInfo url_info = app->InstallChecked(profile());
    app_frame_ = OpenApp(url_info.app_id());

    chromeos::CupsWrapper::SetCupsWrapperFactoryForTesting(
        base::BindRepeating([]() -> std::unique_ptr<chromeos::CupsWrapper> {
          auto wrapper = std::make_unique<MockCupsWrapper>();
          EXPECT_CALL(*wrapper, QueryCupsPrinterStatus(_, _))
              .Times(AtMost(1))
              .WillOnce(base::test::RunOnceCallback<1>([] {
                auto status = std::make_unique<PrinterStatus>();
                status->state = IPP_PSTATE_IDLE;
                status->reasons.push_back(
                    {.reason =
                         PrinterStatus::PrinterReason::Reason::kUnknownReason,
                     .severity =
                         PrinterStatus::PrinterReason::Severity::kReport});
                status->reasons.push_back(
                    {.reason =
                         PrinterStatus::PrinterReason::Reason::kDeveloperLow,
                     .severity =
                         PrinterStatus::PrinterReason::Severity::kWarning});
                status->message = "Ready to Print!";
                return status;
              }()));
          return wrapper;
        }));

    HostContentSettingsMapFactory::GetForProfile(profile())
        ->SetDefaultContentSetting(ContentSettingsType::WEB_PRINTING,
                                   ContentSetting::CONTENT_SETTING_ALLOW);
  }

  void TearDownOnMainThread() override {
    app_frame_ = nullptr;
    chromeos::CupsWrapper::SetCupsWrapperFactoryForTesting(
        base::NullCallback());
  }

 protected:
  content::RenderFrameHost* app_frame() { return app_frame_; }

 private:
  raw_ptr<content::RenderFrameHost> app_frame_ = nullptr;
};

#if BUILDFLAG(IS_CHROMEOS)
class WebPrintingBrowserTest : public WebPrintingBrowserTestBase {
 public:
  void PreRunTestOnMainThread() override {
    WebPrintingBrowserTestBase::PreRunTestOnMainThread();
    helper_->Init(profile());
  }

  void TearDownOnMainThread() override {
    helper_.reset();
    WebPrintingBrowserTestBase::TearDownOnMainThread();
  }

  void SetUpInProcessBrowserTestFixture() override {
    WebPrintingBrowserTestBase::SetUpInProcessBrowserTestFixture();
    helper_ = std::make_unique<extensions::PrintingTestHelper>();
  }

 protected:
  void AddPrinterWithSemanticCaps(
      const std::string& printer_id,
      const std::string& printer_display_name,
      std::unique_ptr<printing::PrinterSemanticCapsAndDefaults> caps) {
    helper_->AddAvailablePrinter(printer_id, printer_display_name,
                                 std::move(caps));
  }

  extensions::PrintingBackendInfrastructureHelper& printing_infra_helper() {
    return helper_->printing_infra_helper();
  }

 private:
  std::unique_ptr<extensions::PrintingTestHelper> helper_;
};
#endif  // BUILDFLAG(IS_CHROMEOS)

IN_PROC_BROWSER_TEST_F(WebPrintingBrowserTest, GetPrinters) {
#if BUILDFLAG(IS_CHROMEOS)
  AddPrinterWithSemanticCaps(kId, kName,
                             extensions::ConstructPrinterCapabilities());
#endif

  constexpr std::string_view kGetPrintersScript = R"(
    (async () => {
      try {
        const printers = await printing.getPrinters();
        if (printers.length !== 1 ||
            printers[0].cachedAttributes().printerName !== $1) {
          return false;
        }
        return true;
      } catch (err) {
        console.log(err);
        return false;
      }
    })();
  )";

  ASSERT_TRUE(EvalJs(app_frame(), content::JsReplace(kGetPrintersScript, kName))
                  .ExtractBool());
}

IN_PROC_BROWSER_TEST_F(WebPrintingBrowserTest, FetchAttributes) {
#if BUILDFLAG(IS_CHROMEOS)
  AddPrinterWithSemanticCaps(kId, kName,
                             extensions::ConstructPrinterCapabilities());
#endif

  // Keep in sync with extensions::ConstructPrinterCapabilities().
  constexpr std::string_view kExpectedAttributes = R"({
    "copiesDefault": 1,
    "copiesSupported": {
      "from": 1,
      "to": 2
    },
    "documentFormatDefault": "application/pdf",
    "documentFormatSupported": [ "application/pdf" ],
    "mediaColDefault": {
      "mediaSize": {
        "xDimension": 21000,
        "yDimension": 29700,
      },
      "mediaSizeName": "iso_a4_210x297mm",
    },
    "mediaColDatabase": [{
      "mediaSize": {
        "xDimension": 21000,
        "yDimension": 29700,
      },
      "mediaSizeName": "iso_a4_210x297mm",
    }, {
      "mediaSize": {
        "xDimension": 21590,
        "yDimension": 27940,
      },
      "mediaSizeName": "na_letter_8.5x11in",
    }, {
      "mediaSize": {
        "xDimension": 20000,
        "yDimension": {
          "from": 25000,
          "to": 30000,
        },
      },
      "mediaSizeName": "om_200000x250000um_200x250mm",
    }],
    "mediaSourceDefault": "auto",
    "mediaSourceSupported": [ "auto", "tray-1" ],
    "multipleDocumentHandlingDefault": "separate-documents-uncollated-copies",
    "multipleDocumentHandlingSupported": [
      "separate-documents-uncollated-copies",
      "separate-documents-collated-copies"
    ],
    "orientationRequestedDefault": "portrait",
    "orientationRequestedSupported": [ "portrait", "landscape" ],
    "printerResolutionDefault": {
      "crossFeedDirectionResolution": 300,
      "feedDirectionResolution": 400,
      "units": "dots-per-inch",
    },
    "printerResolutionSupported": [{
      "crossFeedDirectionResolution": 300,
      "feedDirectionResolution": 400,
      "units": "dots-per-inch",
    }],
    "printColorModeDefault": "monochrome",
    "printColorModeSupported": [ "monochrome", "color" ],
    "printQualityDefault": "draft",
    "printQualitySupported": [ "draft", "normal" ],
    "printerName": "name",
    "printerState": "idle",
    "printerStateMessage": "Ready to Print!",
    "printerStateReasons": [ "other", "developer-low" ],
    "sidesDefault": "one-sided",
    "sidesSupported": [ "one-sided" ]
  })";

  constexpr std::string_view kFetchAttributesScript = R"(
    (async () => {
      const printers = await printing.getPrinters();
      return await printers[0].fetchAttributes();
    })();
  )";

  auto eval_result = EvalJs(app_frame(), kFetchAttributesScript);

  EXPECT_THAT(eval_result.ExtractDict(),
              base::test::DictionaryHasValues(
                  base::test::ParseJsonDict(kExpectedAttributes)));
}

IN_PROC_BROWSER_TEST_F(WebPrintingBrowserTest, Print) {
#if BUILDFLAG(IS_CHROMEOS)
  // Set up a matcher to validate correctness of `PrintSettings`.
  base::MockRepeatingCallback<void(PrintJob*, PrintedDocument*, int)>
      doc_done_cb;
  EXPECT_CALL(
      doc_done_cb,
      Run(_, Property(&PrintedDocument::settings, ValidatePrintSettings()), _));
  auto subscription =
      g_browser_process->print_job_manager()->AddDocDoneCallback(
          doc_done_cb.Get());

  AddPrinterWithSemanticCaps(kId, kName,
                             extensions::ConstructPrinterCapabilities());
#endif

  const auto script = content::JsReplace(kPrintScriptWithJobStatePlaceholder,
                                         /*job_state=*/"completed");
  ASSERT_TRUE(content::ExecJs(app_frame(), script));
}

IN_PROC_BROWSER_TEST_F(WebPrintingBrowserTest, PrintFailure) {
#if BUILDFLAG(IS_CHROMEOS)
  AddPrinterWithSemanticCaps(kId, kName,
                             extensions::ConstructPrinterCapabilities());
#endif
  printing_infra_helper()
      .test_printing_context_factory()
      .SetFailedErrorOnNewDocument(/*cause_errors=*/true);

  const auto script = content::JsReplace(kPrintScriptWithJobStatePlaceholder,
                                         /*job_state=*/"aborted");
  ASSERT_TRUE(content::ExecJs(app_frame(), script));
}

// Validate that call to `printing.getPrinters()` fails when content
// setting is set to BLOCK.
IN_PROC_BROWSER_TEST_F(WebPrintingBrowserTest,
                       GetPrintersUserPermissionDenied) {
  HostContentSettingsMapFactory::GetForProfile(profile())
      ->SetDefaultContentSetting(ContentSettingsType::WEB_PRINTING,
                                 ContentSetting::CONTENT_SETTING_BLOCK);

  constexpr std::string_view kGetPrintersScript = R"(
    (async () => {
      const printers = await printing.getPrinters();
    })();
  )";

  ASSERT_THAT(
      EvalJs(app_frame(), kGetPrintersScript),
      content::EvalJsResult::ErrorIs(testing::HasSubstr("User denied access")));
}

// Validate that further calls to printer's methods fail when content setting
// gets switched to BLOCK after a successful call to
// `printing.getPrinters()`.
IN_PROC_BROWSER_TEST_F(WebPrintingBrowserTest,
                       FetchAndPrintUserPermissionDenied) {
#if BUILDFLAG(IS_CHROMEOS)
  AddPrinterWithSemanticCaps(kId, kName,
                             extensions::ConstructPrinterCapabilities());
#endif

  // Call `printing.getPrinters()` while the permission is active.
  constexpr std::string_view kGetPrintersScript = R"(
    (async () => {
      const printers = await printing.getPrinters();
      printer = printers[0];
    })();
  )";
  ASSERT_TRUE(content::ExecJs(app_frame(), kGetPrintersScript));

  HostContentSettingsMapFactory::GetForProfile(profile())
      ->SetDefaultContentSetting(ContentSettingsType::WEB_PRINTING,
                                 ContentSetting::CONTENT_SETTING_BLOCK);

  // Ensure that `printer.fetchAttributes()` reports access denied.
  constexpr std::string_view kFetchAttributesScript = R"(
    (async () => {
      await printer.fetchAttributes();
    })();
  )";
  ASSERT_THAT(
      EvalJs(app_frame(), kFetchAttributesScript),
      content::EvalJsResult::ErrorIs(testing::HasSubstr("User denied access")));

  // Ensure that `printer.submitPrintJob()` reports access denied.
  constexpr std::string_view kPrintJobScript = R"(
    (async () => {
      const pdf = `%PDF-1.0
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 ` +
`obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 ` +
`obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
xref
0 4
0000000000 65535 f
0000000010 00000 n
0000000053 00000 n
0000000102 00000 n
trailer<</Size 4/Root 1 0 R>>
startxref
149
%EOF`;
      const pdfBlob = new Blob([pdf], {type: 'application/pdf'});
      const printJob = await printer.submitPrintJob("Fail", pdfBlob, {});
    })();
  )";
  ASSERT_THAT(
      EvalJs(app_frame(), kPrintJobScript),
      content::EvalJsResult::ErrorIs(testing::HasSubstr("User denied access")));
}

IN_PROC_BROWSER_TEST_F(WebPrintingBrowserTest, CancelImmediately) {
#if BUILDFLAG(IS_CHROMEOS)
  AddPrinterWithSemanticCaps(kId, kName,
                             extensions::ConstructPrinterCapabilities());
#endif
  constexpr std::string_view kCancelEarlyScript = R"(
    (async () => {
      const pdf = `%PDF-1.0
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 ` +
`obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 ` +
`obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
xref
0 4
0000000000 65535 f
0000000010 00000 n
0000000053 00000 n
0000000102 00000 n
trailer<</Size 4/Root 1 0 R>>
startxref
149
%EOF`;

    const pdfBlob = new Blob([pdf], {type: 'application/pdf'});
    const printers = await printing.getPrinters();

    const printJob = await printers[0].submitPrintJob("Title", pdfBlob, {});
    let phase = 0;
    const printJobCanceled = new Promise((resolve, reject) => {
      printJob.onjobstatechange = () => {
        const state = printJob.attributes().jobState;
        if (state === 'pending') {
          if (phase !== 0) {
            throw new Error('Wrong sequence: kPending should come first.');
            return;
          }
          phase += 1;
        } else if (state === 'canceled') {
          if (phase !== 1) {
            throw new Error('Wrong sequence: kCanceled should come second.');
            return;
          }
          resolve();
        }
      };
    });
    printJob.cancel();
    await printJobCanceled;
   })();
  )";
  ASSERT_TRUE(content::ExecJs(app_frame(), kCancelEarlyScript));
}

IN_PROC_BROWSER_TEST_F(WebPrintingBrowserTest, CancelHalfway) {
#if BUILDFLAG(IS_CHROMEOS)
  AddPrinterWithSemanticCaps(kId, kName,
                             extensions::ConstructPrinterCapabilities());
#endif
  constexpr std::string_view kCancelHalfwayScript = R"(
    (async () => {
      const pdf = `%PDF-1.0
1 0 obj<</Type/Catalog/Pages 2 0 R>>endobj 2 0 ` +
`obj<</Type/Pages/Kids[3 0 R]/Count 1>>endobj 3 0 ` +
`obj<</Type/Page/MediaBox[0 0 3 3]>>endobj
xref
0 4
0000000000 65535 f
0000000010 00000 n
0000000053 00000 n
0000000102 00000 n
trailer<</Size 4/Root 1 0 R>>
startxref
149
%EOF`;

    const pdfBlob = new Blob([pdf], {type: 'application/pdf'});
    const printers = await printing.getPrinters();

    const printJob = await printers[0].submitPrintJob("Title", pdfBlob, {});
    let phase = 0;
    const printJobProcessingThenCanceled = new Promise((resolve, reject) => {
      printJob.onjobstatechange = () => {
        const state = printJob.attributes().jobState;
        if (state === 'processing') {
          if (phase !== 0) {
            throw new Error('Wrong sequence: kProcessing should come first.');
            return;
          }
          phase += 1;
          printJob.cancel();
        } else if (state === 'canceled') {
          if (phase !== 1) {
            throw new Error('Wrong sequence: kCanceled should come second.');
            return;
          }
          resolve();
        }
      };
    });
    await printJobProcessingThenCanceled;
   })();
  )";
  ASSERT_TRUE(content::ExecJs(app_frame(), kCancelHalfwayScript));
}

}  // namespace printing
