// 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 <limits>

#include "base/base_paths.h"
#include "base/files/file_path.h"
#include "base/files/file_util.h"
#include "base/functional/callback_helpers.h"
#include "base/path_service.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/threading/thread_restrictions.h"
#include "base/time/time.h"
#include "content/public/browser/service_process_host.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/content_browser_test.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/data_decoder/public/cpp/data_decoder.h"
#include "services/data_decoder/public/cpp/decode_image.h"
#include "services/data_decoder/public/mojom/data_decoder_service.mojom.h"
#include "services/data_decoder/public/mojom/image_decoder.mojom.h"
#include "services/data_decoder/public/mojom/json_parser.mojom.h"
#include "testing/gmock/include/gmock/gmock.h"

using ::testing::Pair;
using ::testing::UnorderedElementsAre;

namespace content {

namespace {

// Populates `output` and returns true on success (i.e. if `relative_path`
// exists and can be read into `output`).  Otherwise returns false.
bool ReadTestFile(const base::FilePath& relative_path,
                  std::vector<uint8_t>& output) {
  base::FilePath source_root_dir;
  if (!base::PathService::Get(base::DIR_SRC_TEST_DATA_ROOT, &source_root_dir)) {
    return false;
  }

  std::string file_contents_as_string;
  {
    base::ScopedAllowBlockingForTesting allow_file_io_for_testing;
    base::FilePath absolute_path = source_root_dir.Append(relative_path);
    if (!base::ReadFileToString(absolute_path, &file_contents_as_string))
      return false;
  }

  // Convert chars to uint8_ts.
  for (const char& c : file_contents_as_string)
    output.push_back(c);

  return true;
}

// Populates `out_measurement_value` and returns true on success (i.e. if the
// `metric_name` has a single measurement in `histograms`).  Otherwise returns
// false.
bool GetSingleMeasurement(const base::HistogramTester& histograms,
                          const char* metric_name,
                          base::TimeDelta& out_measurement_value) {
  DCHECK(metric_name);

  std::vector<base::Bucket> buckets = histograms.GetAllSamples(metric_name);
  if (buckets.size() != 1u)
    return false;

  EXPECT_EQ(1u, buckets.size());
  out_measurement_value = base::Milliseconds(buckets.front().min);
  return true;
}

}  // namespace

using DataDecoderBrowserTest = ContentBrowserTest;

class ServiceProcessObserver : public ServiceProcessHost::Observer {
 public:
  ServiceProcessObserver() { ServiceProcessHost::AddObserver(this); }

  ServiceProcessObserver(const ServiceProcessObserver&) = delete;
  ServiceProcessObserver& operator=(const ServiceProcessObserver&) = delete;

  ~ServiceProcessObserver() override {
    ServiceProcessHost::RemoveObserver(this);
  }

  int instances_started() const { return instances_started_; }

  void WaitForNextLaunch() {
    launch_wait_loop_.emplace();
    launch_wait_loop_->Run();
  }

  void OnServiceProcessLaunched(const ServiceProcessInfo& info) override {
    if (info.IsService<data_decoder::mojom::DataDecoderService>()) {
      ++instances_started_;
      if (launch_wait_loop_)
        launch_wait_loop_->Quit();
    }
  }

 private:
  std::optional<base::RunLoop> launch_wait_loop_;
  int instances_started_ = 0;
};

IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, Launch) {
  ServiceProcessObserver observer;

  // Verifies that the DataDecoder client object launches a service process as
  // needed.
  data_decoder::DataDecoder decoder;

  // |GetService()| must always ensure a connection to the service on all
  // platforms, so we use it instead of a more specific API whose behavior may
  // vary across platforms.
  decoder.GetService();

  observer.WaitForNextLaunch();
  EXPECT_EQ(1, observer.instances_started());
}

IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, LaunchIsolated) {
  ServiceProcessObserver observer;

  // Verifies that separate DataDecoder client objects will launch separate
  // service processes. We also bind a JsonParser interface to ensure that the
  // instances don't go idle.
  data_decoder::DataDecoder decoder1;
  mojo::Remote<data_decoder::mojom::JsonParser> parser1;
  decoder1.GetService()->BindJsonParser(parser1.BindNewPipeAndPassReceiver());
  observer.WaitForNextLaunch();
  EXPECT_EQ(1, observer.instances_started());

  data_decoder::DataDecoder decoder2;
  mojo::Remote<data_decoder::mojom::JsonParser> parser2;
  decoder2.GetService()->BindJsonParser(parser2.BindNewPipeAndPassReceiver());
  observer.WaitForNextLaunch();
  EXPECT_EQ(2, observer.instances_started());

  // Both interfaces should be connected end-to-end.
  parser1.FlushForTesting();
  parser2.FlushForTesting();
  EXPECT_TRUE(parser1.is_connected());
  EXPECT_TRUE(parser2.is_connected());
}

IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, DecodeImageIsolated) {
  std::vector<uint8_t> file_contents;
  base::FilePath content_test_data_path = GetTestDataFilePath();
  base::FilePath png_path =
      content_test_data_path.AppendASCII("site_isolation/png-corp.png");
  ASSERT_TRUE(ReadTestFile(png_path, file_contents));

  base::HistogramTester histograms;
  {
    base::RunLoop run_loop;
    data_decoder::DecodeImageCallback callback =
        base::BindLambdaForTesting([&](const SkBitmap& decoded_bitmap) {
          EXPECT_EQ(100, decoded_bitmap.width());
          EXPECT_EQ(100, decoded_bitmap.height());
          run_loop.Quit();
        });
    data_decoder::DecodeImageIsolated(
        file_contents, data_decoder::mojom::ImageCodec::kDefault,
        false,                                 // shrink_to_fit
        std::numeric_limits<uint32_t>::max(),  // max_size_in_bytes
        gfx::Size(),                           // desired_image_frame_size
        std::move(callback));
    run_loop.Run();
  }

  FetchHistogramsFromChildProcesses();
  EXPECT_THAT(
      histograms.GetTotalCountsForPrefix("Security.DataDecoder"),
      UnorderedElementsAre(
          Pair("Security.DataDecoder.Image.Isolated.EndToEndTime", 1),
          Pair("Security.DataDecoder.Image.Isolated.ProcessOverhead", 1),
          Pair("Security.DataDecoder.Image.DecodingTime", 1)));

  base::TimeDelta end_to_end_duration_estimate;
  EXPECT_TRUE(GetSingleMeasurement(
      histograms, "Security.DataDecoder.Image.Isolated.EndToEndTime",
      end_to_end_duration_estimate));

  base::TimeDelta overhead_estimate;
  EXPECT_TRUE(GetSingleMeasurement(
      histograms, "Security.DataDecoder.Image.Isolated.ProcessOverhead",
      overhead_estimate));

  base::TimeDelta decoding_duration_estimate;
  EXPECT_TRUE(GetSingleMeasurement(histograms,
                                   "Security.DataDecoder.Image.DecodingTime",
                                   decoding_duration_estimate));

  EXPECT_LE(decoding_duration_estimate, end_to_end_duration_estimate);
  EXPECT_LE(overhead_estimate, end_to_end_duration_estimate);
}

IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, DecodeImage) {
  std::vector<uint8_t> file_contents;
  base::FilePath content_test_data_path = GetTestDataFilePath();
  base::FilePath png_path =
      content_test_data_path.AppendASCII("site_isolation/png-corp.png");
  ASSERT_TRUE(ReadTestFile(png_path, file_contents));

  base::HistogramTester histograms;
  {
    base::RunLoop run_loop;
    data_decoder::DecodeImageCallback callback =
        base::BindLambdaForTesting([&](const SkBitmap& decoded_bitmap) {
          EXPECT_EQ(100, decoded_bitmap.width());
          EXPECT_EQ(100, decoded_bitmap.height());
          run_loop.Quit();
        });

    data_decoder::DataDecoder decoder;
    data_decoder::DecodeImage(
        &decoder, file_contents, data_decoder::mojom::ImageCodec::kDefault,
        false,                                 // shrink_to_fit
        std::numeric_limits<uint32_t>::max(),  // max_size_in_bytes
        gfx::Size(),                           // desired_image_frame_size
        std::move(callback));
    run_loop.Run();
  }

  FetchHistogramsFromChildProcesses();
  EXPECT_THAT(
      histograms.GetTotalCountsForPrefix("Security.DataDecoder"),
      UnorderedElementsAre(
          Pair("Security.DataDecoder.Image.Reusable.EndToEndTime", 1),
          Pair("Security.DataDecoder.Image.Reusable.ProcessOverhead", 1),
          Pair("Security.DataDecoder.Image.DecodingTime", 1)));

  base::TimeDelta end_to_end_duration_estimate;
  EXPECT_TRUE(GetSingleMeasurement(
      histograms, "Security.DataDecoder.Image.Reusable.EndToEndTime",
      end_to_end_duration_estimate));

  base::TimeDelta overhead_estimate;
  EXPECT_TRUE(GetSingleMeasurement(
      histograms, "Security.DataDecoder.Image.Reusable.ProcessOverhead",
      overhead_estimate));

  base::TimeDelta decoding_duration_estimate;
  EXPECT_TRUE(GetSingleMeasurement(histograms,
                                   "Security.DataDecoder.Image.DecodingTime",
                                   decoding_duration_estimate));

  EXPECT_LE(decoding_duration_estimate, end_to_end_duration_estimate);
  EXPECT_LE(overhead_estimate, end_to_end_duration_estimate);
}

IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest,
                       NoCallbackAfterDestruction_Json) {
  base::RunLoop run_loop;

  auto decoder = std::make_unique<data_decoder::DataDecoder>();
  auto* raw_decoder = decoder.get();

  // Android's in-process parser can complete synchronously, so queue the
  // delete task first unlike in the other tests.
  base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(
      FROM_HERE, std::move(decoder));

  bool got_callback = false;
  raw_decoder->ParseJson(
      "[1, 2, 3]",
      base::BindOnce(
          [](bool* got_callback, base::ScopedClosureRunner quit_closure_runner,
             data_decoder::DataDecoder::ValueOrError result) {
            *got_callback = true;
          },
          // Pass the quit closure as a ScopedClosureRunner, so that the loop
          // is quit if the callback is destroyed un-run or after it runs.
          &got_callback, base::ScopedClosureRunner(run_loop.QuitClosure())));

  run_loop.Run();

  EXPECT_FALSE(got_callback);
}

IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest, NoCallbackAfterDestruction_Xml) {
  base::RunLoop run_loop;

  auto decoder = std::make_unique<data_decoder::DataDecoder>();
  bool got_callback = false;
  decoder->ParseXml(
      "<marquee>hello world</marquee>",
      data_decoder::mojom::XmlParser::WhitespaceBehavior::kIgnore,
      base::BindOnce(
          [](bool* got_callback, base::ScopedClosureRunner quit_closure_runner,
             data_decoder::DataDecoder::ValueOrError result) {
            *got_callback = true;
          },
          // Pass the quit closure as a ScopedClosureRunner, so that the loop
          // is quit if the callback is destroyed un-run or after it runs.
          &got_callback, base::ScopedClosureRunner(run_loop.QuitClosure())));

  base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(
      FROM_HERE, std::move(decoder));
  run_loop.Run();

  EXPECT_FALSE(got_callback);
}

IN_PROC_BROWSER_TEST_F(DataDecoderBrowserTest,
                       NoCallbackAfterDestruction_Gzip) {
  base::RunLoop run_loop;

  auto decoder = std::make_unique<data_decoder::DataDecoder>();
  bool got_callback = false;
  decoder->GzipCompress(
      {{0x1, 0x1, 0x1, 0x1, 0x1, 0x1}},
      base::BindOnce(
          [](bool* got_callback, base::ScopedClosureRunner quit_closure_runner,
             base::expected<mojo_base::BigBuffer, std::string> result) {
            *got_callback = true;
          },
          // Pass the quit closure as a ScopedClosureRunner, so that the loop
          // is quit if the callback is destroyed un-run or after it runs.
          &got_callback, base::ScopedClosureRunner(run_loop.QuitClosure())));

  base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(
      FROM_HERE, std::move(decoder));
  run_loop.Run();

  EXPECT_FALSE(got_callback);
}

}  // namespace content