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

#include "components/exo/data_offer.h"

#include <fcntl.h>
#include <stdio.h>

#include <cstdint>
#include <memory>
#include <string>
#include <variant>
#include <vector>

#include "base/compiler_specific.h"
#include "base/containers/flat_set.h"
#include "base/containers/span.h"
#include "base/containers/to_vector.h"
#include "base/files/file_util.h"
#include "base/pickle.h"
#include "base/run_loop.h"
#include "base/strings/string_view_util.h"
#include "base/strings/utf_string_conversions.h"
#include "cc/test/pixel_comparator.h"
#include "cc/test/pixel_test_utils.h"
#include "components/exo/data_device.h"
#include "components/exo/data_exchange_delegate.h"
#include "components/exo/data_offer_delegate.h"
#include "components/exo/test/exo_test_base.h"
#include "components/exo/test/exo_test_data_exchange_delegate.h"
#include "components/exo/test/test_data_offer_delegate.h"
#include "components/exo/test/test_security_delegate.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/clipboard/clipboard.h"
#include "ui/base/clipboard/clipboard_format_type.h"
#include "ui/base/clipboard/scoped_clipboard_writer.h"
#include "ui/base/data_transfer_policy/data_transfer_policy_controller.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/gfx/codec/png_codec.h"
#include "url/gurl.h"

namespace exo {
namespace {

using test::TestDataOfferDelegate;

class DataOfferTest : public test::ExoTestBase {
 public:
  void TearDown() override {
    ui::Clipboard::DestroyClipboardForCurrentThread();
    test::ExoTestBase::TearDown();
  }
};

class TestDataTransferPolicyController : ui::DataTransferPolicyController {
 public:
  TestDataTransferPolicyController() = default;
  TestDataTransferPolicyController(TestDataTransferPolicyController&) = delete;
  TestDataTransferPolicyController& operator=(
      const TestDataTransferPolicyController&) = delete;

  ui::EndpointType last_src_type() const { return last_src_type_; }
  ui::EndpointType last_dst_type() const { return last_dst_type_; }

 private:
  // ui::DataTransferPolicyController:
  bool IsClipboardReadAllowed(
      base::optional_ref<const ui::DataTransferEndpoint> data_src,
      base::optional_ref<const ui::DataTransferEndpoint> data_dst,
      const std::optional<size_t> size) override {
    if (data_src.has_value()) {
      last_src_type_ = data_src->type();
    }
    last_dst_type_ = data_dst->type();
    return true;
  }

  void PasteIfAllowed(
      base::optional_ref<const ui::DataTransferEndpoint> data_src,
      base::optional_ref<const ui::DataTransferEndpoint> data_dst,
      std::variant<size_t, std::vector<base::FilePath>> pasted_content,
      content::RenderFrameHost* web_contents,
      base::OnceCallback<void(bool)> callback) override {}

  void DropIfAllowed(std::optional<ui::DataTransferEndpoint> data_src,
                     std::optional<ui::DataTransferEndpoint> data_dst,
                     std::optional<std::vector<ui::FileInfo>> filenames,
                     base::OnceClosure drop_cb) override {
    std::move(drop_cb).Run();
  }

  ui::EndpointType last_src_type_ = ui::EndpointType::kUnknownVm;
  ui::EndpointType last_dst_type_ = ui::EndpointType::kUnknownVm;
};

std::vector<uint8_t> ReadFD(base::ScopedFD fd) {
  std::vector<uint8_t> buffer(1024);
  uint8_t* it = buffer.data();
  uint8_t* end = UNSAFE_TODO(it + buffer.size());
  while (it != end) {
    int result = read(fd.get(), it, end - it);
    PCHECK(-1 != result);
    if (result == 0) {
      break;
    }
    UNSAFE_TODO(it += result);
  }
  buffer.resize(it - buffer.data());
  return buffer;
}

bool ReadString(base::ScopedFD fd, std::string* out) {
  std::vector<uint8_t> buffer = ReadFD(std::move(fd));
  *out = std::string(base::as_string_view(buffer));
  return true;
}

bool ReadString16(base::ScopedFD fd, std::u16string* out) {
  std::vector<uint8_t> buffer = ReadFD(std::move(fd));
  *out = std::u16string(base::as_string_view(
      base::subtle::reinterpret_span<const char16_t>(base::span(buffer))));
  return true;
}

TEST_F(DataOfferTest, SetTextDropData) {
  base::flat_set<DndAction> source_actions;
  source_actions.insert(DndAction::kCopy);
  source_actions.insert(DndAction::kMove);

  ui::OSExchangeData data;
  data.SetString(std::u16string(u"Test data"));

  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  EXPECT_EQ(0u, delegate.mime_types().size());
  EXPECT_EQ(0u, delegate.source_actions().size());
  EXPECT_EQ(DndAction::kNone, delegate.dnd_action());

  TestDataExchangeDelegate data_exchange_delegate;
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);
  data_offer.SetSourceActions(source_actions);
  data_offer.SetActions(base::flat_set<DndAction>(), DndAction::kMove);

  EXPECT_EQ(1u, delegate.mime_types().count("text/plain;charset=utf-8"));
  EXPECT_EQ(1u, delegate.mime_types().count("text/plain;charset=utf-16"));
  EXPECT_EQ(2u, delegate.source_actions().size());
  EXPECT_EQ(1u, delegate.source_actions().count(DndAction::kCopy));
  EXPECT_EQ(1u, delegate.source_actions().count(DndAction::kMove));
  EXPECT_EQ(DndAction::kMove, delegate.dnd_action());
}

TEST_F(DataOfferTest, SetHTMLDropData) {
  const std::string html_data = "Test HTML data 🔥 ❄";

  base::flat_set<DndAction> source_actions;
  source_actions.insert(DndAction::kCopy);
  source_actions.insert(DndAction::kMove);

  ui::OSExchangeData data;
  data.SetHtml(base::UTF8ToUTF16(html_data), GURL());

  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  EXPECT_EQ(0u, delegate.mime_types().size());
  EXPECT_EQ(0u, delegate.source_actions().size());
  EXPECT_EQ(DndAction::kNone, delegate.dnd_action());

  TestDataExchangeDelegate data_exchange_delegate;
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);
  data_offer.SetSourceActions(source_actions);
  data_offer.SetActions(base::flat_set<DndAction>(), DndAction::kMove);

  EXPECT_EQ(1u, delegate.mime_types().count("text/html;charset=utf-8"));
  EXPECT_EQ(1u, delegate.mime_types().count("text/html;charset=utf-16"));
  EXPECT_EQ(2u, delegate.source_actions().size());
  EXPECT_EQ(1u, delegate.source_actions().count(DndAction::kCopy));
  EXPECT_EQ(1u, delegate.source_actions().count(DndAction::kMove));
  EXPECT_EQ(DndAction::kMove, delegate.dnd_action());

  base::ScopedFD read, write;
  std::string result;
  EXPECT_TRUE(base::CreatePipe(&read, &write));
  data_offer.Receive("text/html;charset=utf-8", std::move(write));
  ReadString(std::move(read), &result);
  EXPECT_EQ(result, html_data);

  std::u16string result16;
  EXPECT_TRUE(base::CreatePipe(&read, &write));
  data_offer.Receive("text/html;charset=utf-16", std::move(write));
  ReadString16(std::move(read), &result16);
  EXPECT_EQ(result16, base::UTF8ToUTF16(html_data));
}

TEST_F(DataOfferTest, SetFileDropData) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  ui::OSExchangeData data;
  data.SetFilename(base::FilePath("/test/downloads/file"));
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);

  EXPECT_EQ(1u, delegate.mime_types().size());
  EXPECT_EQ(1u, delegate.mime_types().count("text/uri-list"));
}

TEST_F(DataOfferTest, SetPickleDropData) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  ui::OSExchangeData data;

  base::Pickle pickle;
  pickle.WriteUInt32(1);  // num files
  pickle.WriteString("filesystem:chrome-extension://path/to/file1");
  pickle.WriteInt64(1000);   // file size
  pickle.WriteString("id");  // filesystem id
  data.SetPickledData(ui::ClipboardFormatType::CustomPlatformType(
                          "chromium/x-file-system-files"),
                      pickle);
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);

  EXPECT_EQ(1u, delegate.mime_types().size());
  EXPECT_EQ(1u, delegate.mime_types().count("text/uri-list"));
}

TEST_F(DataOfferTest, SetFileContentsDropData) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  ui::OSExchangeData data;
  data.provider().SetFileContents(base::FilePath("\"test file\".jpg"),
                                  base::byte_span_from_cstring("test data"));
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);

  EXPECT_EQ(1u, delegate.mime_types().size());
  EXPECT_EQ(1u, delegate.mime_types().count(
                    "application/octet-stream;name=\"\\\"test file\\\".jpg\""));
}

TEST_F(DataOfferTest, ReceiveString) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  ui::OSExchangeData data;
  data.SetString(u"Test data");
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);

  base::ScopedFD read_pipe;
  base::ScopedFD write_pipe;
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));

  data_offer.Receive("text/plain", std::move(write_pipe));
  std::string result;
  ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
  EXPECT_EQ("Test data", result);

  base::ScopedFD read_pipe_16;
  base::ScopedFD write_pipe_16;
  ASSERT_TRUE(base::CreatePipe(&read_pipe_16, &write_pipe_16));
  data_offer.Receive("text/plain;charset=utf-16", std::move(write_pipe_16));
  std::u16string result_16;
  ASSERT_TRUE(ReadString16(std::move(read_pipe_16), &result_16));
  EXPECT_EQ(u"Test data", result_16);

  base::ScopedFD read_pipe_8;
  base::ScopedFD write_pipe_8;
  ASSERT_TRUE(base::CreatePipe(&read_pipe_8, &write_pipe_8));
  data_offer.Receive("text/plain;charset=utf-8", std::move(write_pipe_8));
  std::string result_8;
  ASSERT_TRUE(ReadString(std::move(read_pipe_8), &result_8));
  EXPECT_EQ("Test data", result_8);
}

TEST_F(DataOfferTest, ReceiveHTML) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  ui::OSExchangeData data;
  data.SetHtml(u"Test HTML data", GURL());
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);

  base::ScopedFD read_pipe_16;
  base::ScopedFD write_pipe_16;
  ASSERT_TRUE(base::CreatePipe(&read_pipe_16, &write_pipe_16));
  data_offer.Receive("text/html;charset=utf-16", std::move(write_pipe_16));
  std::u16string result_16;
  ASSERT_TRUE(ReadString16(std::move(read_pipe_16), &result_16));
  EXPECT_EQ(u"Test HTML data", result_16);

  base::ScopedFD read_pipe_8;
  base::ScopedFD write_pipe_8;
  ASSERT_TRUE(base::CreatePipe(&read_pipe_8, &write_pipe_8));
  data_offer.Receive("text/html;charset=utf-8", std::move(write_pipe_8));
  std::string result_8;
  ASSERT_TRUE(ReadString(std::move(read_pipe_8), &result_8));
  EXPECT_EQ("Test HTML data", result_8);
}

TEST_F(DataOfferTest, ReceiveUriList) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  ui::OSExchangeData data;
  data.SetFilename(base::FilePath("/test/downloads/file"));
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);

  base::ScopedFD read_pipe;
  base::ScopedFD write_pipe;
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));

  data_offer.Receive("text/uri-list", std::move(write_pipe));
  std::string result;
  ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
  EXPECT_EQ("file:///test/downloads/file", result);
}

TEST_F(DataOfferTest, ReceiveUriListFromPickle_ReceiveBeforeUrlIsResolved) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  ui::OSExchangeData data;

  base::Pickle pickle;
  pickle.WriteUInt32(1);  // num files
  pickle.WriteString("filesystem:chrome-extension://path/to/file1");
  pickle.WriteInt64(1000);   // file size
  pickle.WriteString("id");  // filesystem id
  data.SetPickledData(ui::ClipboardFormatType::CustomPlatformType(
                          "chromium/x-file-system-files"),
                      pickle);
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);

  base::ScopedFD read_pipe1;
  base::ScopedFD write_pipe1;
  ASSERT_TRUE(base::CreatePipe(&read_pipe1, &write_pipe1));
  base::ScopedFD read_pipe2;
  base::ScopedFD write_pipe2;
  ASSERT_TRUE(base::CreatePipe(&read_pipe2, &write_pipe2));

  // Receive is called (twice) before UrlsFromPickleCallback runs.
  data_offer.Receive("text/uri-list", std::move(write_pipe1));
  data_offer.Receive("text/uri-list", std::move(write_pipe2));

  // Run callback with a resolved URL.
  std::vector<GURL> urls;
  urls.push_back(
      GURL("content://org.chromium.arc.chromecontentprovider/path/to/file1"));
  delegate.GetSecurityDelegate()->RunSendPickleCallback(urls);

  std::string result1;
  ASSERT_TRUE(ReadString(std::move(read_pipe1), &result1));
  EXPECT_EQ("content://org.chromium.arc.chromecontentprovider/path/to/file1",
            result1);
  std::string result2;
  ASSERT_TRUE(ReadString(std::move(read_pipe2), &result2));
  EXPECT_EQ("content://org.chromium.arc.chromecontentprovider/path/to/file1",
            result2);
}

TEST_F(DataOfferTest,
       ReceiveUriListFromPickle_ReceiveBeforeEmptyUrlIsReturned) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  ui::OSExchangeData data;

  base::Pickle pickle;
  pickle.WriteUInt32(1);  // num files
  pickle.WriteString("filesystem:chrome-extension://path/to/file1");
  pickle.WriteInt64(1000);   // file size
  pickle.WriteString("id");  // filesystem id
  data.SetPickledData(ui::ClipboardFormatType::CustomPlatformType(
                          "chromium/x-file-system-files"),
                      pickle);
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);

  base::ScopedFD read_pipe;
  base::ScopedFD write_pipe;
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));

  // Receive is called before UrlsCallback runs.
  data_offer.Receive("text/uri-list", std::move(write_pipe));

  // Run callback with an empty URL.
  std::vector<GURL> urls;
  urls.push_back(GURL(""));
  delegate.GetSecurityDelegate()->RunSendPickleCallback(urls);

  std::u16string result;
  ASSERT_TRUE(ReadString16(std::move(read_pipe), &result));
  EXPECT_EQ(u"", result);
}

TEST_F(DataOfferTest, ReceiveFileContentsDropData) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  ui::OSExchangeData data;
  const std::vector<uint8_t> expected =
      base::ToVector(base::byte_span_from_cstring("test data"));
  data.provider().SetFileContents(base::FilePath("test.jpg"), expected);
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);

  base::ScopedFD read_pipe;
  base::ScopedFD write_pipe;
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));

  data_offer.Receive("application/octet-stream;name=\"test.jpg\"",
                     std::move(write_pipe));
  EXPECT_EQ(expected, ReadFD(std::move(read_pipe)));
}

TEST_F(DataOfferTest, SetClipboardDataPlainText) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  {
    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
    writer.WriteText(u"Test data");
  }

  auto* window = CreateTestWindowInShell({}).release();
  data_offer.SetClipboardData(
      &data_exchange_delegate, *ui::Clipboard::GetForCurrentThread(),
      data_exchange_delegate.GetDataTransferEndpointType(window));

  EXPECT_EQ(3u, delegate.mime_types().size());
  EXPECT_EQ(1u, delegate.mime_types().count("text/plain;charset=utf-8"));
  EXPECT_EQ(1u, delegate.mime_types().count("text/plain;charset=utf-16"));
  EXPECT_EQ(1u, delegate.mime_types().count("UTF8_STRING"));

  base::ScopedFD read_pipe;
  base::ScopedFD write_pipe;

  // Read as utf-8.
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
  data_offer.Receive("text/plain;charset=utf-8", std::move(write_pipe));
  std::string result;
  ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
  EXPECT_EQ("Test data", result);

  // Read a second time.
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
  data_offer.Receive("text/plain;charset=utf-8", std::move(write_pipe));
  ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
  EXPECT_EQ("Test data", result);

  // Read as utf-16.
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
  data_offer.Receive("text/plain;charset=utf-16", std::move(write_pipe));
  std::u16string result16;
  ASSERT_TRUE(ReadString16(std::move(read_pipe), &result16));
  EXPECT_EQ("Test data", base::UTF16ToUTF8(result16));
}

TEST_F(DataOfferTest, SetClipboardDataHTML) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  {
    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
    writer.WriteHTML(u"Test data", "");
  }

  auto* window = CreateTestWindowInShell({}).release();
  data_offer.SetClipboardData(
      &data_exchange_delegate, *ui::Clipboard::GetForCurrentThread(),
      data_exchange_delegate.GetDataTransferEndpointType(window));

  EXPECT_EQ(3u, delegate.mime_types().size());
  EXPECT_EQ(1u, delegate.mime_types().count("text/html;charset=utf-8"));
  EXPECT_EQ(1u, delegate.mime_types().count("text/html;charset=utf-16"));
  EXPECT_EQ(1u, delegate.mime_types().count("text/html"));

  base::ScopedFD read_pipe;
  base::ScopedFD write_pipe;
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));

  data_offer.Receive("text/html;charset=utf-8", std::move(write_pipe));
  std::string result;
  ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
  EXPECT_EQ("Test data", result);

  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
  data_offer.Receive("text/html;charset=utf-16", std::move(write_pipe));
  std::u16string result16;
  ASSERT_TRUE(ReadString16(std::move(read_pipe), &result16));
  EXPECT_EQ("Test data", base::UTF16ToUTF8(result16));

  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
  data_offer.Receive("text/html", std::move(write_pipe));
  ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
  EXPECT_EQ("Test data", result);
}

TEST_F(DataOfferTest, SetClipboardDataRTF) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  {
    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
    writer.WriteRTF("Test data");
  }

  auto* window = CreateTestWindowInShell({}).release();
  data_offer.SetClipboardData(
      &data_exchange_delegate, *ui::Clipboard::GetForCurrentThread(),
      data_exchange_delegate.GetDataTransferEndpointType(window));

  EXPECT_EQ(1u, delegate.mime_types().size());
  EXPECT_EQ(1u, delegate.mime_types().count("text/rtf"));

  base::ScopedFD read_pipe;
  base::ScopedFD write_pipe;
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));

  data_offer.Receive("text/rtf", std::move(write_pipe));
  std::string result;
  ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
  EXPECT_EQ("Test data", result);
}

TEST_F(DataOfferTest, SetClipboardDataImage) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  SkBitmap image;
  image.allocN32Pixels(10, 10);
  image.eraseColor(SK_ColorMAGENTA);

  TestDataExchangeDelegate data_exchange_delegate;
  {
    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
    writer.WriteImage(image);
  }

  auto* window = CreateTestWindowInShell({}).release();
  data_offer.SetClipboardData(
      &data_exchange_delegate, *ui::Clipboard::GetForCurrentThread(),
      data_exchange_delegate.GetDataTransferEndpointType(window));

  EXPECT_EQ(1u, delegate.mime_types().size());
  EXPECT_EQ(1u, delegate.mime_types().count("image/png"));

  base::ScopedFD read_pipe;
  base::ScopedFD write_pipe;
  base::ScopedFD read_pipe2;
  base::ScopedFD write_pipe2;
  std::string result;

  // Call Receive() twice in quick succession. Requires RunUntilIdle() since
  // processing is done on worker thread.
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
  ASSERT_TRUE(base::CreatePipe(&read_pipe2, &write_pipe2));
  data_offer.Receive("image/png", std::move(write_pipe));
  data_offer.Receive("image/png", std::move(write_pipe2));
  task_environment()->RunUntilIdle();
  ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
  SkBitmap decoded = gfx::PNGCodec::Decode(base::as_byte_span(result));
  ASSERT_FALSE(decoded.isNull());
  EXPECT_TRUE(cc::MatchesBitmap(image, decoded, cc::ExactPixelComparator()));
  std::string good = result;
  ASSERT_TRUE(ReadString(std::move(read_pipe2), &result));
  EXPECT_EQ(good, result);

  // Receive() should now return immediately with result from cache.
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
  data_offer.Receive("image/png", std::move(write_pipe));
  ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
  EXPECT_EQ(good, result);
}

TEST_F(DataOfferTest, SetClipboardDataFilenames) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  {
    ui::ScopedClipboardWriter writer(ui::ClipboardBuffer::kCopyPaste);
    writer.WriteFilenames("file:///test/path");
  }

  auto* window = CreateTestWindowInShell({}).release();
  data_offer.SetClipboardData(
      &data_exchange_delegate, *ui::Clipboard::GetForCurrentThread(),
      data_exchange_delegate.GetDataTransferEndpointType(window));

  EXPECT_EQ(1u, delegate.mime_types().size());
  EXPECT_EQ(1u, delegate.mime_types().count("text/uri-list"));

  base::ScopedFD read_pipe;
  base::ScopedFD write_pipe;
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));

  data_offer.Receive("text/uri-list", std::move(write_pipe));
  std::string result;
  ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
  EXPECT_EQ("file:///test/path", result);
}

TEST_F(DataOfferTest, AcceptWithNull) {
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);
  data_offer.Accept(nullptr);
}

TEST_F(DataOfferTest, SetClipboardDataWithTransferPolicy) {
  TestDataTransferPolicyController policy_controller;
  TestDataOfferDelegate delegate;
  DataOffer data_offer(&delegate);

  TestDataExchangeDelegate data_exchange_delegate;
  data_exchange_delegate.set_endpoint_type(ui::EndpointType::kCrostini);
  {
    ui::ScopedClipboardWriter writer(
        ui::ClipboardBuffer::kCopyPaste,
        std::make_unique<ui::DataTransferEndpoint>(ui::EndpointType::kArc));
    writer.WriteText(u"Test data");
  }

  auto* window = CreateTestWindowInShell({}).release();
  data_offer.SetClipboardData(
      &data_exchange_delegate, *ui::Clipboard::GetForCurrentThread(),
      data_exchange_delegate.GetDataTransferEndpointType(window));

  EXPECT_EQ(3u, delegate.mime_types().size());
  EXPECT_EQ(1u, delegate.mime_types().count("text/plain;charset=utf-8"));
  EXPECT_EQ(1u, delegate.mime_types().count("text/plain;charset=utf-16"));
  EXPECT_EQ(1u, delegate.mime_types().count("UTF8_STRING"));

  base::ScopedFD read_pipe;
  base::ScopedFD write_pipe;

  // Read as utf-8.
  ASSERT_TRUE(base::CreatePipe(&read_pipe, &write_pipe));
  data_offer.Receive("text/plain;charset=utf-8", std::move(write_pipe));
  std::string result;
  ASSERT_TRUE(ReadString(std::move(read_pipe), &result));
  EXPECT_EQ("Test data", result);

  EXPECT_EQ(ui::EndpointType::kArc, policy_controller.last_src_type());
  EXPECT_EQ(ui::EndpointType::kCrostini, policy_controller.last_dst_type());
}

TEST_F(DataOfferTest, TestDelegateDestroyed) {
  auto delegate = std::make_unique<TestDataOfferDelegate>();
  DataOffer data_offer(delegate.get());

  // Destroy the delegate. S now has a dead delegate.
  delegate.reset();

  // Call methods on data_offer. They should not crash.
  base::flat_set<DndAction> source_actions;
  source_actions.insert(DndAction::kCopy);
  data_offer.SetSourceActions(source_actions);
  data_offer.SetActions(base::flat_set<DndAction>(), DndAction::kMove);

  TestDataExchangeDelegate data_exchange_delegate;
  ui::OSExchangeData data;
  data.SetString(std::u16string(u"Test data"));
  data_offer.SetDropData(&data_exchange_delegate, nullptr, data);

  // Testing SetClipboardData is harder because it's async and uses clipboard,
  // but we can at least call it and it shouldn't crash immediately.
  data_offer.SetClipboardData(&data_exchange_delegate,
                              *ui::Clipboard::GetForCurrentThread(),
                              ui::EndpointType::kCrostini);
}

}  // namespace
}  // namespace exo
