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

#import "ui/views/cocoa/drag_drop_client_mac.h"

#import <Cocoa/Cocoa.h>

#import "base/apple/scoped_objc_class_swizzler.h"
#include "base/functional/bind.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/utf_string_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/run_until.h"
#import "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#import "ui/base/clipboard/clipboard_util_mac.h"
#import "ui/base/dragdrop/drag_drop_types.h"
#import "ui/base/dragdrop/mojom/drag_drop_types.mojom.h"
#include "ui/base/metadata/metadata_header_macros.h"
#include "ui/base/metadata/metadata_impl_macros.h"
#include "ui/compositor/layer_tree_owner.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/gfx/image/image_unittest_util.h"
#import "ui/views/cocoa/native_widget_mac_ns_window_host.h"
#include "ui/views/test/widget_test.h"
#include "ui/views/view.h"
#include "ui/views/widget/native_widget_mac.h"
#include "ui/views/widget/widget.h"

namespace {
int g_begin_dragging_session_call_count = 0;
}  // namespace

@interface NSView (DragSessionTestingDonor)
@end

@implementation NSView (DragSessionTestingDonor)
- (NSDraggingSession*)cr_beginDraggingSessionWithItems:(NSArray*)items
                                                 event:(NSEvent*)event
                                                source:(id<NSDraggingSource>)
                                                           source {
  ++g_begin_dragging_session_call_count;
  return nil;
}
@end

// Mocks the NSDraggingInfo sent to the DragDropClientMac's DragUpdate() and
// Drop() methods. Out of the required methods of the protocol, only
// draggingLocation and draggingPasteboard are used.
@interface MockDraggingInfo : NSObject <NSDraggingInfo>

@property BOOL animatesToDestination;
@property NSInteger numberOfValidItemsForDrop;
@property NSDraggingFormation draggingFormation;
@property(readonly) NSSpringLoadingHighlight springLoadingHighlight;

@end

@implementation MockDraggingInfo {
  NSPasteboard* _pasteboard;
}

@synthesize animatesToDestination;
@synthesize numberOfValidItemsForDrop;
@synthesize draggingFormation;
@synthesize springLoadingHighlight;

- (instancetype)initWithPasteboard:(NSPasteboard*)pasteboard {
  if ((self = [super init])) {
    _pasteboard = pasteboard;
  }
  return self;
}

- (NSPoint)draggingLocation {
  return NSMakePoint(50, 50);
}

- (NSPasteboard*)draggingPasteboard {
  return _pasteboard;
}

- (NSInteger)draggingSequenceNumber {
  return 0;
}

- (id)draggingSource {
  return nil;
}

- (NSDragOperation)draggingSourceOperationMask {
  return NSDragOperationEvery;
}

- (NSWindow*)draggingDestinationWindow {
  return nil;
}

- (NSArray*)namesOfPromisedFilesDroppedAtDestination:(NSURL*)dropDestination {
  return nil;
}

- (NSImage*)draggedImage {
  return nil;
}

- (NSPoint)draggedImageLocation {
  return NSZeroPoint;
}

- (void)slideDraggedImageTo:(NSPoint)aPoint {
}

- (void)
    enumerateDraggingItemsWithOptions:(NSDraggingItemEnumerationOptions)enumOpts
                              forView:(NSView*)view
                              classes:(NSArray*)classArray
                        searchOptions:(NSDictionary*)searchOptions
                           usingBlock:(void (^)(NSDraggingItem* draggingItem,
                                                NSInteger idx,
                                                BOOL* stop))block {
}

- (void)resetSpringLoading {
}

@end

namespace views::test {

using ::ui::mojom::DragOperation;

// View object that will receive and process dropped data from the test.
class DragDropView : public View {
  METADATA_HEADER(DragDropView, View)

 public:
  DragDropView() = default;

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

  void set_formats(int formats) { formats_ = formats; }

  // View:
  bool GetDropFormats(
      int* formats,
      std::set<ui::ClipboardFormatType>* format_types) override {
    *formats |= formats_;
    return true;
  }

  bool CanDrop(const OSExchangeData& data) override { return true; }

  int OnDragUpdated(const ui::DropTargetEvent& event) override {
    return ui::DragDropTypes::DRAG_COPY;
  }

  views::View::DropCallback GetDropCallback(
      const ui::DropTargetEvent& event) override {
    return base::BindOnce(
        [](const ui::DropTargetEvent& event,
           ui::mojom::DragOperation& output_drag_op,
           std::unique_ptr<ui::LayerTreeOwner> drag_image_layer_owner) {
          output_drag_op = DragOperation::kMove;
        });
  }

 private:
  // Drop formats accepted by this View object.
  int formats_ = 0;
};

BEGIN_METADATA(DragDropView)
END_METADATA

class DragDropClientMacTest : public WidgetTest {
 public:
  DragDropClientMacTest() = default;

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

  DragDropClientMac* drag_drop_client() {
    return ns_window_host()->drag_drop_client();
  }

  NSDragOperation DragUpdate(NSPasteboard* pasteboard) {
    DragDropClientMac* client = drag_drop_client();
    dragging_info_ = [[MockDraggingInfo alloc] initWithPasteboard:pasteboard];
    return client->DragUpdate(dragging_info_);
  }

  NSDragOperation Drop() {
    DragDropClientMac* client = drag_drop_client();
    DCHECK(dragging_info_);
    NSDragOperation operation = client->Drop(dragging_info_);
    dragging_info_ = nil;
    return operation;
  }

  void SetData(OSExchangeData& data) {
    drag_drop_client()->exchange_data_ =
        std::make_unique<ui::OSExchangeData>(data.provider().Clone());
  }

  // testing::Test:
  void SetUp() override {
    WidgetTest::SetUp();

    widget_ = CreateTopLevelPlatformWidget()->GetWeakPtr();
    gfx::Rect bounds(0, 0, 100, 100);
    widget_->SetBounds(bounds);
    widget_->Show();

    target_ = widget_->non_client_view()->frame_view()->AddChildView(
        std::make_unique<DragDropView>());
    target_->SetBoundsRect(bounds);

    drag_drop_client()->source_operation_ = ui::DragDropTypes::DRAG_COPY;
  }

  void TearDown() override {
    target_ = nullptr;
    if (widget_) {
      widget_->CloseNow();
    }
    WidgetTest::TearDown();
  }

  remote_cocoa::NativeWidgetNSWindowBridge* bridge() {
    return ns_window_host()->GetInProcessNSWindowBridge();
  }

  NativeWidgetMacNSWindowHost* ns_window_host() {
    return NativeWidgetMacNSWindowHost::GetFromNativeWindow(
        widget_->GetNativeWindow());
  }

 protected:
  base::WeakPtr<Widget> widget_;
  raw_ptr<DragDropView> target_ = nullptr;

 private:
  MockDraggingInfo* __strong dragging_info_;
};

// Tests if the drag and drop target receives the dropped data.
TEST_F(DragDropClientMacTest, BasicDragDrop) {
  // Create the drop data
  OSExchangeData data;
  const std::u16string& text = u"text";
  data.SetString(text);
  SetData(data);

  target_->set_formats(ui::OSExchangeData::STRING | ui::OSExchangeData::URL);

  // Check if the target receives the data from a drop and returns the expected
  // operation.
  EXPECT_EQ(DragUpdate(nil), NSDragOperationCopy);
  EXPECT_EQ(Drop(), NSDragOperationMove);
}

// Ensure that capture is released before the end of a drag and drop operation.
TEST_F(DragDropClientMacTest, ReleaseCapture) {
  // DragDropView doesn't actually capture the mouse, so explicitly acquire it
  // to test that StartDragAndDrop() actually releases it.
  // Although this is not an interactive UI test, acquiring capture should be OK
  // since the runloop will exit before the system has any opportunity to
  // capture anything.
  bridge()->AcquireCapture();
  EXPECT_TRUE(ns_window_host()->IsMouseCaptureActive());

  // Create the drop data
  std::unique_ptr<OSExchangeData> data(std::make_unique<OSExchangeData>());
  const std::u16string& text = u"text";
  data->SetString(text);
  data->provider().SetDragImage(gfx::test::CreateImageSkia(100, 100),
                                gfx::Vector2d());
  SetData(*data.get());

  // There's no way to cleanly stop NSDraggingSession inside unit tests, so just
  // don't start it at all.
  base::apple::ScopedObjCClassSwizzler swizzle(
      [NSView class], @selector(beginDraggingSessionWithItems:event:source:),
      @selector(cr_beginDraggingSessionWithItems:event:source:));

  // Immediately quit drag'n'drop, or we'll hang.
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(&DragDropClientMac::EndDrag,
                                base::Unretained(drag_drop_client())));

  // It will call ReleaseCapture().
  drag_drop_client()->StartDragAndDrop(std::move(data), 0,
                                       ui::mojom::DragEventSource::kMouse);

  // The capture should be released.
  EXPECT_FALSE(ns_window_host()->IsMouseCaptureActive());
}

// A drag should not start while a window move loop is running for any window
// in the process. The move loop already owns the held mouse button and a
// concurrent dragging session would interfere with it.
TEST_F(DragDropClientMacTest, DragRejectedDuringWindowMoveLoop) {
  std::unique_ptr<OSExchangeData> data = std::make_unique<OSExchangeData>();
  data->SetString(u"text");
  data->provider().SetDragImage(gfx::test::CreateImageSkia(100, 100),
                                gfx::Vector2d());

  // There's no way to cleanly stop NSDraggingSession inside unit tests, so just
  // don't start it at all.
  g_begin_dragging_session_call_count = 0;
  base::apple::ScopedObjCClassSwizzler swizzle(
      [NSView class], @selector(beginDraggingSessionWithItems:event:source:),
      @selector(cr_beginDraggingSessionWithItems:event:source:));

  // Attempt to start a drag from inside the move loop and then end the move
  // loop.
  bool drag_attempted = false;
  base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE,
      base::BindOnce(
          [](DragDropClientMac* client,
             remote_cocoa::NativeWidgetNSWindowBridge* bridge,
             std::unique_ptr<OSExchangeData> data, bool* drag_attempted) {
            client->StartDragAndDrop(std::move(data),
                                     ui::DragDropTypes::DRAG_COPY,
                                     ui::mojom::DragEventSource::kMouse);
            *drag_attempted = true;
            bridge->EndMoveLoop();
          },
          drag_drop_client(), bridge(), std::move(data), &drag_attempted));

  bridge()->RunMoveLoop(gfx::Vector2d());

  EXPECT_TRUE(base::test::RunUntil([&]() { return drag_attempted; }));
  EXPECT_EQ(0, g_begin_dragging_session_call_count);
}

// Tests if the drag and drop target rejects the dropped data with the
// incorrect format.
TEST_F(DragDropClientMacTest, InvalidFormatDragDrop) {
  OSExchangeData data;
  const std::u16string& text = u"text";
  data.SetString(text);
  SetData(data);

  target_->set_formats(ui::OSExchangeData::URL);

  // Check if the target receives the data from a drop and returns the expected
  // operation.
  EXPECT_EQ(DragUpdate(nil), NSDragOperationNone);
  EXPECT_EQ(Drop(), NSDragOperationNone);
}

// Tests if the drag and drop target can accept data without an OSExchangeData
// object.
TEST_F(DragDropClientMacTest, PasteboardToOSExchangeTest) {
  target_->set_formats(ui::OSExchangeData::STRING);

  scoped_refptr<ui::UniquePasteboard> pasteboard = new ui::UniquePasteboard;

  // The test should reject the data if the pasteboard is empty.
  EXPECT_EQ(DragUpdate(pasteboard->get()), NSDragOperationNone);
  EXPECT_EQ(Drop(), NSDragOperationNone);
  drag_drop_client()->EndDrag();

  // Add valid data to the pasteboard and check to see if the target accepts
  // it.
  [pasteboard->get() setString:@"text" forType:NSPasteboardTypeString];
  EXPECT_EQ(DragUpdate(pasteboard->get()), NSDragOperationCopy);
  EXPECT_EQ(Drop(), NSDragOperationMove);
}

// View object that will close Widget on drop.
class DragDropCloseView : public DragDropView {
  METADATA_HEADER(DragDropCloseView, DragDropView)
 public:
  DragDropCloseView() = default;

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

  views::View::DropCallback GetDropCallback(
      const ui::DropTargetEvent& event) override {
    // base::Unretained is safe here because in the tests the view isn't deleted
    // before the drop callback is run.
    return base::BindOnce(&DragDropCloseView::PerformDrop,
                          base::Unretained(this));
  }

 private:
  void PerformDrop(const ui::DropTargetEvent& event,
                   DragOperation& output_drag_op,
                   std::unique_ptr<ui::LayerTreeOwner> drag_image_layer_owner) {
    GetWidget()->CloseNow();
    output_drag_op = DragOperation::kMove;
  }
};

BEGIN_METADATA(DragDropCloseView)
END_METADATA

// Tests that closing Widget on drop does not crash.
TEST_F(DragDropClientMacTest, CloseWidgetOnDrop) {
  OSExchangeData data;
  const std::u16string& text = u"text";
  data.SetString(text);
  SetData(data);

  DragDropCloseView* target =
      widget_->non_client_view()->frame_view()->AddChildView(
          std::make_unique<DragDropCloseView>());
  target->SetBoundsRect(gfx::Rect(0, 0, 100, 100));
  target->set_formats(ui::OSExchangeData::STRING | ui::OSExchangeData::URL);

  // Dropping will destroy target_.
  target_ = nullptr;
  EXPECT_EQ(DragUpdate(nil), NSDragOperationCopy);
  EXPECT_EQ(Drop(), NSDragOperationMove);
}

}  // namespace views::test
