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

#import "content/browser/web_contents/web_drag_dest_mac.h"

#include <AppKit/AppKit.h>
#import <Carbon/Carbon.h>

#include "base/apple/bridging.h"
#include "base/apple/scoped_nsautorelease_pool.h"
#include "base/files/file_util.h"
#include "base/files/scoped_temp_dir.h"
#include "base/logging.h"
#include "base/mac/mac_util.h"
#include "base/memory/ref_counted.h"
#include "base/memory/stack_allocated.h"
#include "base/strings/sys_string_conversions.h"
#import "content/browser/web_contents/web_contents_view_mac.h"
#include "content/public/browser/web_drag_dest_delegate.h"
#include "content/public/common/drop_data.h"
#include "content/test/test_render_view_host.h"
#include "content/test/test_web_contents.h"
#include "testing/gtest/include/gtest/gtest.h"
#import "ui/base/clipboard/clipboard_util_mac.h"
#import "ui/base/test/cocoa_helper.h"

class WebDragDestTest : public content::RenderViewHostImplTestHarness {
 public:
  void SetUp() override {
    content::RenderViewHostImplTestHarness::SetUp();
    drag_dest_ = [[WebDragDest alloc] initWithWebContentsImpl:contents()];
  }

  STACK_ALLOCATED_IGNORE("https://crbug.com/1424190")
  base::apple::ScopedNSAutoreleasePool pool_;
  WebDragDest* __strong drag_dest_;
};

// Make sure nothing leaks.
TEST_F(WebDragDestTest, Init) {
  EXPECT_TRUE(drag_dest_);
}

TEST_F(WebDragDestTest, EndDragSync) {
  // When there is no pending drop operation, calling `endDrag` should just
  // synchronously call any passed callback. If it didn't, the test would fail
  // from `run_loop` timing out.
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];
  run_loop.Run();
}

TEST_F(WebDragDestTest, EndDragAsync) {
  // When there is a pending drop operation, calling `endDrag` should store the
  // passed callback and call it after `finishDropWithData:context:` resolves.
  [drag_dest_ setDropInProgressForTesting];

  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];

  EXPECT_FALSE(run_loop.AnyQuitCalled());
  EXPECT_TRUE([drag_dest_ dropInProgressForTesting]);

  [drag_dest_
      finishDropWithData:std::nullopt
                 context:content::DropContext(content::DropData(),
                                              gfx::PointF(), gfx::PointF(), 0,
                                              main_test_rfh()
                                                  ->GetRenderWidgetHost()
                                                  ->GetWeakPtr())];

  run_loop.Run();
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);
}

TEST_F(WebDragDestTest, Data) {
  scoped_refptr<ui::UniquePasteboard> pboard = new ui::UniquePasteboard;
  NSString* html_string = @"<html><body><b>hi there</b></body></html>";
  NSString* text_string = @"hi there";
  [pboard->get() setString:@"http://www.google.com"
                   forType:NSPasteboardTypeURL];
  [pboard->get() setString:html_string forType:NSPasteboardTypeHTML];
  [pboard->get() setString:text_string forType:NSPasteboardTypeString];

  content::DropData data =
      content::PopulateDropDataFromPasteboard(pboard->get());

  EXPECT_EQ(data.url_infos.front().url.spec(), "http://www.google.com/");
  EXPECT_EQ(base::SysNSStringToUTF16(text_string), data.text);
  EXPECT_EQ(base::SysNSStringToUTF16(html_string), data.html);
}

TEST_F(WebDragDestTest, UTTypeImageFallback) {
  scoped_refptr<ui::UniquePasteboard> pboard = new ui::UniquePasteboard;
  std::string image_bytes = "fake image data";
  NSData* raw_data = [NSData dataWithBytes:image_bytes.data()
                                    length:image_bytes.size()];
  [pboard->get() setData:raw_data forType:@"public.jpeg"];

  content::DropData data =
      content::PopulateDropDataFromPasteboard(pboard->get());

  EXPECT_EQ(std::string(data.file_contents.begin(), data.file_contents.end()),
            image_bytes);
  EXPECT_EQ(data.file_contents_filename_extension, "jpeg");
}

TEST_F(WebDragDestTest, FileURLAndFilePromiseContentDoNotDuplicate) {
  scoped_refptr<ui::UniquePasteboard> pboard = new ui::UniquePasteboard;

  // Build a single pasteboard item that advertises both a real file URL and
  // file-promise content. This mirrors a screenshot dragged from macOS's
  // Screenshot tool before it has been saved to disk: the same file is
  // represented two ways on one pasteboard item.
  NSPasteboardItem* item = [[NSPasteboardItem alloc] init];
  NSURL* file_url = [NSURL fileURLWithPath:@"/tmp/screenshot.png"];
  [item setString:file_url.absoluteString forType:NSPasteboardTypeFileURL];
  [item
      setString:@"public.png"
        forType:base::apple::CFToNSPtrCast(kPasteboardTypeFilePromiseContent)];
  std::string image_bytes = "fake image data";
  NSData* raw_data = [NSData dataWithBytes:image_bytes.data()
                                    length:image_bytes.size()];
  [item setData:raw_data forType:@"public.png"];
  [pboard->get() writeObjects:@[ item ]];

  content::DropData data =
      content::PopulateDropDataFromPasteboard(pboard->get());

  EXPECT_EQ(data.filenames.size(), 1u);
  EXPECT_TRUE(data.file_contents.empty());
}

TEST_F(WebDragDestTest, FilePromiseContentWithoutFileURL) {
  scoped_refptr<ui::UniquePasteboard> pboard = new ui::UniquePasteboard;

  // A promise-only item (no backing file URL on disk), e.g. a JS File object
  // dragged from web content.
  NSPasteboardItem* item = [[NSPasteboardItem alloc] init];
  [item
      setString:@"public.png"
        forType:base::apple::CFToNSPtrCast(kPasteboardTypeFilePromiseContent)];
  std::string image_bytes = "fake image data";
  NSData* raw_data = [NSData dataWithBytes:image_bytes.data()
                                    length:image_bytes.size()];
  [item setData:raw_data forType:@"public.png"];
  [pboard->get() writeObjects:@[ item ]];

  content::DropData data =
      content::PopulateDropDataFromPasteboard(pboard->get());

  EXPECT_TRUE(data.filenames.empty());
  EXPECT_EQ(std::string(data.file_contents.begin(), data.file_contents.end()),
            image_bytes);
}

TEST_F(WebDragDestTest, FilePermissions) {
  // Set up a temporary directory
  base::ScopedTempDir temp_dir;
  ASSERT_TRUE(temp_dir.CreateUniqueTempDir());

  // Define the path for the new temporary file
  base::FilePath temp_file_path = temp_dir.GetPath().AppendASCII("test_file");

  // Set a custom umask (disables write permission for group and others) and
  // store the original umask
  const mode_t kCustomUmask = 0022;
  mode_t original_umask = umask(kCustomUmask);

  // Create the temporary file
  base::File file(temp_file_path,
                  base::File::FLAG_CREATE | base::File::FLAG_WRITE);
  ASSERT_TRUE(file.IsValid());

  // Invoke the function to set file permissions
  content::WebContentsViewMac::SetReadWritePermissionsForFileForTests(file);

  // Restore the original umask
  umask(original_umask);

  // Retrieve and verify the file permissions
  int mode = 0;
  ASSERT_TRUE(base::GetPosixFilePermissions(temp_file_path, &mode));

  // Calculate the expected permissions considering the umask
  int expected_permissions = 0666 & ~kCustomUmask;

  EXPECT_EQ(mode, expected_permissions);
}

// Test that drop data survives draggingExited when drop is about to occur
TEST_F(WebDragDestTest, DropDataSurvivedDraggingExited) {
  // Set up drop data
  content::DropData drop_data;
  drop_data.text = u"test text";
  drop_data.url_infos.push_back(
      ui::ClipboardUrlInfo{GURL("http://example.com"), u"Example"});
  [drag_dest_ setDropData:drop_data];

  // Simulate draggingEntered to initialize state
  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  [drag_dest_ draggingEntered:&info];

  // Wait for async hit test callback to complete.
  // RunUntilIdle is appropriate here because we're waiting for an async
  // callback that will be posted to the task queue.
  base::RunLoop().RunUntilIdle();

  // Current drop data should exist
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Simulate draggingExited - this clears drop data
  [drag_dest_ draggingExited];

  // Drop data should be cleared after draggingExited
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);
}

// Test that endDrag preserves drop data when drop is in progress
TEST_F(WebDragDestTest, EndDragPreservesDropDataDuringDrop) {
  // Set up drop data
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  // Simulate drag enter
  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;
  [drag_dest_ draggingEntered:&info];

  // Set drop in progress (simulating performDragOperation)
  [drag_dest_ setDropInProgressForTesting];

  // Verify drop is in progress
  EXPECT_TRUE([drag_dest_ dropInProgressForTesting]);

  // Call endDrag with drop in progress
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];

  // Closure should NOT have run yet (stored for later)
  EXPECT_FALSE(run_loop.AnyQuitCalled());

  // Drop data should still exist
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Complete the drop
  [drag_dest_ finishDropWithData:drop_data
                         context:content::DropContext(
                                     drop_data, gfx::PointF(), gfx::PointF(), 0,
                                     main_test_rfh()
                                         ->GetRenderWidgetHost()
                                         ->GetWeakPtr())];

  // Now closure should run
  run_loop.Run();
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);
}

// Test that endDrag clears data immediately when no drop is in progress
TEST_F(WebDragDestTest, EndDragClearsDataWhenNoDropInProgress) {
  // Set up drop data
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  // Simulate drag enter
  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;
  [drag_dest_ draggingEntered:&info];

  // Verify data exists
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Call endDrag without drop in progress
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];

  // Closure should run immediately
  run_loop.Run();

  // Drop data should be cleared now
  // Note: currentDropData returns the filtered data, which should be null
  // after endDrag clears it
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);
}

// Test the complete drop flow: enter -> update -> exit -> perform -> end
TEST_F(WebDragDestTest, CompleteDropFlow) {
  // Set up drop data
  content::DropData drop_data;
  drop_data.text = u"test text";
  drop_data.url_infos.push_back(
      ui::ClipboardUrlInfo{GURL("http://example.com"), u"Example"});
  [drag_dest_ setDropData:drop_data];

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  // 1. Drag enters
  NSDragOperation op = [drag_dest_ draggingEntered:&info];
  EXPECT_EQ(NSDragOperationCopy, op);

  // Wait for async hit test callback to complete.
  // RunUntilIdle is appropriate here because we're waiting for an async
  // callback that will be posted to the task queue.
  base::RunLoop().RunUntilIdle();

  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // 2. Drag updates
  info.location_in_view = gfx::PointF(20, 20);
  op = [drag_dest_ draggingUpdated:&info];
  EXPECT_NE(NSDragOperationNone, op);

  // 3. Drag exits - this clears drop data
  [drag_dest_ draggingExited];
  // Data should be cleared after exit
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  // 4. Set drop data again before perform (simulating real drop flow)
  [drag_dest_ setDropData:drop_data];

  // Perform drop (without delegate = synchronous completion)
  BOOL accepted = [drag_dest_ performDragOperation:&info
                       withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted);

  // Wait for async hit test to complete
  base::RunLoop().RunUntilIdle();

  // After async callback completes with no delegate, drop completes
  // synchronously so the flag should now be false
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);

  // 5. End drag (Mac calls this after drop)
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];

  // Should complete immediately since drop already finished
  run_loop.Run();
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);
}

// Test drop without delegate (synchronous path)
TEST_F(WebDragDestTest, DropWithoutDelegate) {
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  [drag_dest_ draggingEntered:&info];

  // Perform drop without delegate
  BOOL accepted = [drag_dest_ performDragOperation:&info
                       withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted);

  // Wait for async hit test callback to complete
  base::RunLoop().RunUntilIdle();

  // Since there's no delegate, the drop completes synchronously in the async
  // callback The flag gets cleared in OnDropComplete when there's no delegate
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);
}

// Test multiple rapid drag operations (stale callback scenario)
TEST_F(WebDragDestTest, RapidDragOperations) {
  // First drag
  content::DropData drop_data1;
  drop_data1.text = u"first drag";
  [drag_dest_ setDropData:drop_data1];

  remote_cocoa::mojom::DraggingInfo info1;
  info1.location_in_view = gfx::PointF(10, 10);
  info1.location_in_screen = gfx::PointF(100, 100);
  info1.operation_mask = NSDragOperationCopy;

  [drag_dest_ draggingEntered:&info1];
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Drag exits
  [drag_dest_ draggingExited];

  // End first drag
  base::RunLoop run_loop1;
  [drag_dest_ endDrag:run_loop1.QuitClosure()];
  run_loop1.Run();

  // Second drag starts immediately
  content::DropData drop_data2;
  drop_data2.text = u"second drag";
  [drag_dest_ setDropData:drop_data2];

  remote_cocoa::mojom::DraggingInfo info2;
  info2.location_in_view = gfx::PointF(20, 20);
  info2.location_in_screen = gfx::PointF(120, 120);
  info2.operation_mask = NSDragOperationCopy;

  NSDragOperation op = [drag_dest_ draggingEntered:&info2];
  EXPECT_EQ(NSDragOperationCopy, op);

  // Complete second drag
  [drag_dest_ draggingExited];
  base::RunLoop run_loop2;
  [drag_dest_ endDrag:run_loop2.QuitClosure()];
  run_loop2.Run();
}

// Test that drop data persists through the critical timing window
TEST_F(WebDragDestTest, DropDataPersistsThroughCriticalWindow) {
  content::DropData drop_data;
  drop_data.text = u"persistent data";
  drop_data.url_infos.push_back(
      ui::ClipboardUrlInfo{GURL("http://test.com"), u"Test"});
  [drag_dest_ setDropData:drop_data];

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  // Enter
  [drag_dest_ draggingEntered:&info];

  // Wait for async hit test callback to complete.
  // RunUntilIdle is appropriate here because we're waiting for an async
  // callback that will be posted to the task queue.
  base::RunLoop().RunUntilIdle();

  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Update
  [drag_dest_ draggingUpdated:&info];

  // Wait for async hit test callback to complete.
  // RunUntilIdle is appropriate here because we're waiting for an async
  // callback that will be posted to the task queue.
  base::RunLoop().RunUntilIdle();

  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Exit - this clears drop data
  [drag_dest_ draggingExited];
  // Data should be cleared after exit
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  // Set drop data again before perform (simulating real drop flow)
  [drag_dest_ setDropData:drop_data];

  // Perform drop
  BOOL accepted = [drag_dest_ performDragOperation:&info
                       withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted);

  // End drag
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];

  // Complete drop
  [drag_dest_
      finishDropWithData:drop_data
                 context:content::DropContext(drop_data, gfx::PointF(10, 10),
                                              gfx::PointF(100, 100), 0,
                                              main_test_rfh()
                                                  ->GetRenderWidgetHost()
                                                  ->GetWeakPtr())];

  run_loop.Run();
}

TEST_F(WebDragDestTest, AsyncHitTestRaceCondition) {
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for initial enter to establish state
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Enable deferral for update to create race
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(true);

  // Start drag update - this triggers async hit test
  NSDragOperation op = [drag_dest_ draggingUpdated:&info];
  EXPECT_EQ(NSDragOperationCopy, op);

  // Immediately exit drag BEFORE async hit test callback completes
  // This simulates a race condition where the drag exits before
  // GetRenderWidgetHostAtPointAsynchronously callback is invoked
  [drag_dest_ draggingExited];

  // Drop data should be cleared immediately after exit
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  // Now trigger the deferred callback
  static_cast<content::TestWebContents*>(contents())
      ->TriggerGetRenderWidgetHostAtPointAsynchronouslyCallback();

  // Now let the async hit test callback complete
  base::RunLoop().RunUntilIdle();

  // After async callback completes, drop data should still be null
  // (the callback should handle stale state gracefully)
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  // Clean up
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];
  run_loop.Run();
}

// Test race condition where draggingExited is called before draggingEntered
// async callback completes
TEST_F(WebDragDestTest, DragEnterExitRaceCondition) {
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  // Enable deferral to prevent callback from executing immediately
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(true);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  // Start drag enter - this triggers async hit test
  NSDragOperation op = [drag_dest_ draggingEntered:&info];
  EXPECT_EQ(NSDragOperationCopy, op);

  // Immediately exit drag BEFORE async hit test callback completes
  [drag_dest_ draggingExited];

  // Drop data should be cleared immediately after exit
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  // Now trigger the deferred callback
  static_cast<content::TestWebContents*>(contents())
      ->TriggerGetRenderWidgetHostAtPointAsynchronouslyCallback();

  // Let the async hit test callback complete
  base::RunLoop().RunUntilIdle();

  // After async callback completes, drop data should still be null
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  // Clean up
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];
  run_loop.Run();
}

// Test race condition where performDragOperation is followed by draggingExited
// before the drop async callback completes
TEST_F(WebDragDestTest, PerformDropExitRaceCondition) {
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for initial enter to establish state
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Enable deferral for drop to create race
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(true);

  // Start drop operation - this triggers async hit test
  BOOL accepted = [drag_dest_ performDragOperation:&info
                       withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted);
  EXPECT_TRUE([drag_dest_ dropInProgressForTesting]);

  // Immediately exit drag BEFORE async drop callback completes
  [drag_dest_ draggingExited];

  // Drop data should be cleared immediately after exit
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  // Now trigger the deferred callback
  static_cast<content::TestWebContents*>(contents())
      ->TriggerGetRenderWidgetHostAtPointAsynchronouslyCallback();

  // Let the async drop callback complete
  base::RunLoop().RunUntilIdle();

  // Drop should have completed despite the race
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);

  // Clean up
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];
  run_loop.Run();
}

// Test multiple rapid draggingUpdated calls with deferred callbacks
TEST_F(WebDragDestTest, MultipleUpdatesRaceCondition) {
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for initial enter to establish state
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Enable deferral for updates
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(true);

  // Trigger multiple rapid updates
  info.location_in_view = gfx::PointF(20, 20);
  NSDragOperation op1 = [drag_dest_ draggingUpdated:&info];
  EXPECT_EQ(NSDragOperationCopy, op1);

  info.location_in_view = gfx::PointF(30, 30);
  NSDragOperation op2 = [drag_dest_ draggingUpdated:&info];
  EXPECT_EQ(NSDragOperationCopy, op2);

  info.location_in_view = gfx::PointF(40, 40);
  NSDragOperation op3 = [drag_dest_ draggingUpdated:&info];
  EXPECT_EQ(NSDragOperationCopy, op3);

  // Exit drag before any callbacks complete
  [drag_dest_ draggingExited];
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  // Trigger the deferred callback (only the last one should matter)
  static_cast<content::TestWebContents*>(contents())
      ->TriggerGetRenderWidgetHostAtPointAsynchronouslyCallback();

  // Let callbacks complete
  base::RunLoop().RunUntilIdle();

  // Drop data should still be null
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  // Clean up
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];
  run_loop.Run();
}

// Test race condition where a new drag starts while previous drag's async
// callback is still pending
TEST_F(WebDragDestTest, NewDragDuringPendingCallbackRaceCondition) {
  content::DropData drop_data1;
  drop_data1.text = u"first drag";
  [drag_dest_ setDropData:drop_data1];

  // Enable deferral for first drag
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(true);

  remote_cocoa::mojom::DraggingInfo info1;
  info1.location_in_view = gfx::PointF(10, 10);
  info1.location_in_screen = gfx::PointF(100, 100);
  info1.operation_mask = NSDragOperationCopy;

  // Start first drag
  [drag_dest_ draggingEntered:&info1];

  // Exit first drag immediately
  [drag_dest_ draggingExited];
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  // End first drag
  base::RunLoop run_loop1;
  [drag_dest_ endDrag:run_loop1.QuitClosure()];
  run_loop1.Run();

  // Now start a second drag with new data
  content::DropData drop_data2;
  drop_data2.text = u"second drag";
  [drag_dest_ setDropData:drop_data2];

  // Disable deferral for second drag to make it complete normally
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info2;
  info2.location_in_view = gfx::PointF(20, 20);
  info2.location_in_screen = gfx::PointF(120, 120);
  info2.operation_mask = NSDragOperationCopy;

  // Start second drag
  [drag_dest_ draggingEntered:&info2];
  base::RunLoop().RunUntilIdle();

  // Second drag should have valid data
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Now trigger the stale callback from first drag
  static_cast<content::TestWebContents*>(contents())
      ->TriggerGetRenderWidgetHostAtPointAsynchronouslyCallback();
  base::RunLoop().RunUntilIdle();

  // Second drag's data should still be valid (not affected by stale callback)
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Clean up second drag
  [drag_dest_ draggingExited];
  base::RunLoop run_loop2;
  [drag_dest_ endDrag:run_loop2.QuitClosure()];
  run_loop2.Run();
}

// Custom delegate to track drag enter/leave events
class DragEventTrackingDelegate : public content::WebDragDestDelegate {
 public:
  DragEventTrackingDelegate() = default;
  ~DragEventTrackingDelegate() override = default;

  // WebDragDestDelegate implementation
  void DragInitialize(content::WebContents* contents) override {
    initialize_count_++;
  }
  void OnDragEnter() override { enter_count_++; }
  void OnDragOver() override { over_count_++; }
  void OnDragLeave() override { leave_count_++; }
  void OnDrop() override { drop_count_++; }

  int initialize_count() const { return initialize_count_; }
  int enter_count() const { return enter_count_; }
  int over_count() const { return over_count_; }
  int leave_count() const { return leave_count_; }
  int drop_count() const { return drop_count_; }

  void reset() {
    initialize_count_ = 0;
    enter_count_ = 0;
    over_count_ = 0;
    leave_count_ = 0;
    drop_count_ = 0;
  }

 private:
  int initialize_count_ = 0;
  int enter_count_ = 0;
  int over_count_ = 0;
  int leave_count_ = 0;
  int drop_count_ = 0;
};

// Test that verifies dragEnter is called on new RWH and dragLeave on old RWH
// when target changes during drop operation.
// This tests the specific code path at lines 585-603 in web_drag_dest_mac.mm
TEST_F(WebDragDestTest, DropTargetChangeCallsDragLeaveAndEnter) {
  // Set up a tracking delegate to monitor drag events
  DragEventTrackingDelegate tracking_delegate;
  [drag_dest_ setDragDelegate:&tracking_delegate];

  content::DropData drop_data;
  drop_data.text = u"test text for RWH change";
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for initial enter to establish state
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  // Initial drag enter - this should trigger dragEnter on the main RWH
  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();

  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);
  // Should have called DragInitialize and OnDragEnter
  EXPECT_EQ(1, tracking_delegate.initialize_count());
  EXPECT_EQ(1, tracking_delegate.enter_count());
  EXPECT_EQ(0, tracking_delegate.leave_count());

  // Perform a drag update
  info.location_in_view = gfx::PointF(20, 20);
  [drag_dest_ draggingUpdated:&info];
  base::RunLoop().RunUntilIdle();

  // Should have called OnDragOver
  EXPECT_EQ(1, tracking_delegate.over_count());

  // Now simulate the drop operation
  // The code at lines 585-603 handles the case where targetRWH changes
  // between the last drag update and the drop
  info.location_in_view = gfx::PointF(30, 30);

  // Perform drop without delegate (synchronous path)
  BOOL accepted = [drag_dest_ performDragOperation:&info
                       withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted);

  base::RunLoop().RunUntilIdle();

  // After drop completes, OnDrop should have been called
  EXPECT_EQ(1, tracking_delegate.drop_count());

  // Clean up
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];
  run_loop.Run();

  // The test verifies that the code path handles target changes correctly
  // In the actual implementation, if targetRWH != _currentRWHForDrag.get():
  // 1. DragTargetDragLeave is called on _currentRWHForDrag
  // 2. dragEnterHitTestDidCompleteForView is called with the new target
  // This ensures proper state management when dragging across iframe boundaries
}

// Test that verifies the re-enter logic when drop target changes
TEST_F(WebDragDestTest, DropWithTargetChangeReEntersNewTarget) {
  DragEventTrackingDelegate tracking_delegate;
  [drag_dest_ setDragDelegate:&tracking_delegate];

  content::DropData drop_data;
  drop_data.text = u"test for re-enter";
  drop_data.url_infos.push_back(
      ui::ClipboardUrlInfo{GURL("http://example.com"), u"Example"});
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for predictable execution
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  // Enter and update to establish current RWH
  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(1, tracking_delegate.enter_count());

  [drag_dest_ draggingUpdated:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_EQ(1, tracking_delegate.over_count());

  // Perform drop - if target changed, the code would:
  // 1. Call DragTargetDragLeave on old target
  // 2. Copy _pendingDropInfo to _pendingDragEnteredInfo
  // 3. Call dragEnterHitTestDidCompleteForView with new target
  // 4. Complete the drop on the new target
  BOOL accepted = [drag_dest_ performDragOperation:&info
                       withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted);
  base::RunLoop().RunUntilIdle();

  // Verify drop completed
  EXPECT_EQ(1, tracking_delegate.drop_count());
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);

  // Clean up
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];
  run_loop.Run();
}

// Test the specific code path where _dropDataUnfiltered exists during re-enter
TEST_F(WebDragDestTest, DropTargetChangeWithDropDataUnfiltered) {
  DragEventTrackingDelegate tracking_delegate;
  [drag_dest_ setDragDelegate:&tracking_delegate];

  content::DropData drop_data;
  drop_data.text = u"test with unfiltered data";
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for synchronous execution
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  // Establish drag state
  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // The drop operation tests the code at lines 593-598 which handles:
  // if (_dropDataUnfiltered) {
  //   _pendingDragEnteredInfo =
  //     std::make_unique<remote_cocoa::mojom::DraggingInfo>(*_pendingDropInfo);
  //   [self dragEnterHitTestDidCompleteForView:target_view
  //                           transformedPoint:transformedPoint
  //                             sequenceNumber:++_dragEnteredSequenceNumber];
  // }

  // Perform drop
  BOOL accepted = [drag_dest_ performDragOperation:&info
                       withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted);
  base::RunLoop().RunUntilIdle();

  // Verify the drop completed successfully
  EXPECT_EQ(1, tracking_delegate.drop_count());
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);

  // Clean up
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];
  run_loop.Run();
}

// Test race condition where endDrag is called while drop callback is pending
TEST_F(WebDragDestTest, EndDragDuringPendingDropCallbackRaceCondition) {
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for initial enter
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Enable deferral for drop
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(true);

  // Start drop operation
  BOOL accepted = [drag_dest_ performDragOperation:&info
                       withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted);
  EXPECT_TRUE([drag_dest_ dropInProgressForTesting]);

  // Call endDrag while drop is in progress
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];

  // Closure should not have run yet
  EXPECT_FALSE(run_loop.AnyQuitCalled());
  EXPECT_TRUE([drag_dest_ dropInProgressForTesting]);

  // Now trigger the deferred drop callback
  static_cast<content::TestWebContents*>(contents())
      ->TriggerGetRenderWidgetHostAtPointAsynchronouslyCallback();

  // Let drop callback complete - this should trigger the endDrag closure
  run_loop.Run();

  // Drop should be complete
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);
}
// Test that state cleanup is consistent across error paths
TEST_F(WebDragDestTest, StateCleanupOnInvalidTarget) {
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for initial enter
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();

  // Verify drop data exists
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Simulate drag exit to trigger cleanup
  [drag_dest_ draggingExited];

  // Verify all state is properly cleaned up
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);
}

// Test state cleanup with drag exit after enter completes
TEST_F(WebDragDestTest, StateCleanupAfterDragEnterComplete) {
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for immediate callback
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Exit and verify cleanup
  [drag_dest_ draggingExited];
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);
}

// Test state cleanup when drag update completes then exits
TEST_F(WebDragDestTest, StateCleanupAfterDragUpdateComplete) {
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for initial enter
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  [drag_dest_ draggingUpdated:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Exit and verify cleanup
  [drag_dest_ draggingExited];
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);
}

// Test cleanup helper is invoked during normal drop flow
TEST_F(WebDragDestTest, CleanupHelperDuringDropFlow) {
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for initial enter
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];

  BOOL accepted = [drag_dest_ performDragOperation:&info
                       withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted);

  // With deferral disabled, the drop completes synchronously
  base::RunLoop().RunUntilIdle();

  // Drop should be complete and state cleaned up
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  run_loop.RunUntilIdle();
}

// Test sequence number overflow protection
TEST_F(WebDragDestTest, SequenceNumberOverflowProtection) {
  content::DropData drop_data;
  drop_data.text = u"test text";
  [drag_dest_ setDropData:drop_data];

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  // Disable async behavior for predictable testing
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  // Simulate many drag operations to test overflow handling
  // Note: We can't actually overflow uint64_t in a test, but we can verify
  // the code handles the logic correctly by testing the pattern

  // First operation
  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  [drag_dest_ draggingExited];
  EXPECT_EQ(nullptr, [drag_dest_ currentDropData]);

  // Second operation after exit
  [drag_dest_ setDropData:drop_data];
  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  [drag_dest_ draggingExited];

  // Third operation
  [drag_dest_ setDropData:drop_data];
  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Verify operations continue to work correctly after multiple cycles
  [drag_dest_ draggingUpdated:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);
}

// Test rapid drops without cleanup
TEST_F(WebDragDestTest, RapidDropsWithoutCleanup) {
  content::DropData drop_data;
  drop_data.text = u"test text";

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  // Disable async behavior for predictable testing
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  // First drag-drop cycle
  [drag_dest_ setDropData:drop_data];
  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();

  base::RunLoop run_loop1;
  [drag_dest_ endDrag:run_loop1.QuitClosure()];

  BOOL accepted1 = [drag_dest_ performDragOperation:&info
                        withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted1);
  base::RunLoop().RunUntilIdle();

  // Wait for first drop to complete
  run_loop1.RunUntilIdle();

  // Immediately start second drag-drop cycle without explicit cleanup
  [drag_dest_ setDropData:drop_data];
  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();

  base::RunLoop run_loop2;
  [drag_dest_ endDrag:run_loop2.QuitClosure()];

  BOOL accepted2 = [drag_dest_ performDragOperation:&info
                        withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted2);
  base::RunLoop().RunUntilIdle();

  run_loop2.RunUntilIdle();

  // Verify state is properly managed across rapid operations
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);
}

// Test drop target change between drag update and drop
// This tests the scenario at lines 585-603 in web_drag_dest_mac.mm where
// targetRWH != _currentRWHForDrag.get() during drop.
TEST_F(WebDragDestTest, DropTargetChangeBetweenUpdateAndDrop) {
  content::DropData drop_data;
  drop_data.text = u"test text for target change";
  [drag_dest_ setDropData:drop_data];

  // Disable deferral for initial enter to establish state
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(false);

  remote_cocoa::mojom::DraggingInfo info;
  info.location_in_view = gfx::PointF(10, 10);
  info.location_in_screen = gfx::PointF(100, 100);
  info.operation_mask = NSDragOperationCopy;

  // Initial drag enter
  [drag_dest_ draggingEntered:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Drag update to same location
  [drag_dest_ draggingUpdated:&info];
  base::RunLoop().RunUntilIdle();
  EXPECT_NE(nullptr, [drag_dest_ currentDropData]);

  // Now enable deferral to control the timing of the drop hit test
  static_cast<content::TestWebContents*>(contents())
      ->SetDeferGetRenderWidgetHostAtPoint(true);

  // Simulate performDragOperation - this will trigger async hit test
  BOOL accepted = [drag_dest_ performDragOperation:&info
                       withWebContentsViewDelegate:nullptr];
  EXPECT_TRUE(accepted);
  EXPECT_TRUE([drag_dest_ dropInProgressForTesting]);

  // Trigger the deferred callback which will complete the drop
  // The code path at lines 585-603 handles when the target might have changed
  static_cast<content::TestWebContents*>(contents())
      ->TriggerGetRenderWidgetHostAtPointAsynchronouslyCallback();

  // Let the drop callback complete
  base::RunLoop().RunUntilIdle();

  // Drop should complete successfully even if target changed
  EXPECT_FALSE([drag_dest_ dropInProgressForTesting]);

  // Clean up
  base::RunLoop run_loop;
  [drag_dest_ endDrag:run_loop.QuitClosure()];
  run_loop.Run();
}
