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

#include "chrome/browser/ui/views/tabs/dragging/tab_drag_controller_interactive_uitest.h"

#include <stddef.h>

#include <algorithm>
#include <array>
#include <limits>
#include <memory>
#include <optional>
#include <set>
#include <utility>
#include <vector>

#include "base/command_line.h"
#include "base/compiler_specific.h"
#include "base/dcheck_is_on.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/location.h"
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/scoped_observation.h"
#include "base/strings/string_number_conversions.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/bind.h"
#include "base/test/metrics/histogram_tester.h"
#include "base/test/run_until.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/test_timeouts.h"
#include "base/threading/thread_restrictions.h"
#include "build/build_config.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window/public/browser_collection_observer.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/browser_window/public/create_browser_window.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/tabs/split_tab_metrics.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model_observer.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/frame/browser_frame_view.h"
#include "chrome/browser/ui/views/frame/browser_native_widget_factory.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/tab_strip_region_view.h"
#include "chrome/browser/ui/views/interaction/browser_elements_views.h"
#include "chrome/browser/ui/views/tabs/dragging/tab_drag_controller.h"
#include "chrome/browser/ui/views/tabs/dragging/tab_drag_controller_interactive_test_mixin.h"
#include "chrome/browser/ui/views/tabs/dragging/window_finder.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_strip.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/model/display_override.h"
#include "chrome/browser/web_applications/test/os_integration_test_override_impl.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/interactive_test_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/ui/base/window_properties.h"
#include "components/constrained_window/constrained_window_views.h"
#include "components/prefs/pref_service.h"
#include "components/split_tabs/split_tab_id.h"
#include "components/tab_groups/tab_group_color.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "components/tabs/public/split_tab_data.h"
#include "components/tabs/public/tab_group.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/browser/web_contents.h"
#include "content/public/common/content_features.h"
#include "content/public/common/content_switches.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "third_party/blink/public/common/features.h"
#include "ui/aura/env.h"
#include "ui/base/mojom/ui_base_types.mojom-shared.h"
#include "ui/base/mojom/window_show_state.mojom.h"
#include "ui/base/ozone_buildflags.h"
#include "ui/base/test/ui_controls.h"
#include "ui/compositor/layer.h"
#include "ui/display/display.h"
#include "ui/display/screen.h"
#include "ui/display/screen_base.h"
#include "ui/gfx/animation/animation_test_api.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/native/native_view_host.h"
#include "ui/views/test/widget_activation_waiter.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
#include "ui/views/window/dialog_delegate.h"

#if BUILDFLAG(IS_OZONE)
#include "ui/ozone/public/ozone_platform.h"
#endif

#if BUILDFLAG(IS_MAC)
#include "base/mac/mac_util.h"
#endif

#if defined(USE_AURA)
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/test/test_window_delegate.h"
#include "ui/aura/test/test_windows.h"
#include "ui/aura/window_targeter.h"
#endif

#if defined(USE_AURA) && !BUILDFLAG(IS_CHROMEOS)
#include "chrome/browser/ui/views/frame/browser_native_widget_aura.h"
#include "ui/views/widget/desktop_aura/desktop_native_widget_aura.h"
#endif

#if BUILDFLAG(IS_CHROMEOS)
#include "ash/public/cpp/split_view_test_api.h"
#include "ash/public/cpp/tablet_mode.h"
#include "ash/public/cpp/test/shell_test_api.h"
#include "ash/public/cpp/window_properties.h"
#include "ash/shell.h"
#include "ash/wm/float/float_controller.h"
#include "ash/wm/overview/overview_session.h"
#include "ash/wm/overview/overview_utils.h"
#include "ash/wm/splitview/split_view_controller.h"
#include "ash/wm/splitview/split_view_types.h"
#include "ash/wm/window_state.h"
#include "ash/wm/window_util.h"
#include "base/test/simple_test_tick_clock.h"
#include "chrome/browser/ash/boca/on_task/on_task_locked_controller.h"
#include "chrome/browser/ash/system_web_apps/test_support/test_system_web_app_installation.h"
#include "chrome/browser/ui/immersive/immersive_mode_controller.h"
#include "chrome/browser/ui/views/frame/immersive_mode_controller_chromeos.h"
#include "chromeos/ui/frame/immersive/immersive_fullscreen_controller_test_api.h"
#include "ui/aura/client/cursor_client.h"
#include "ui/aura/client/cursor_shape_client.h"
#include "ui/aura/client/screen_position_client.h"
#include "ui/aura/window_event_dispatcher.h"
#include "ui/base/cursor/cursor.h"
#include "ui/display/manager/display_manager.h"
#include "ui/display/test/display_manager_test_api.h"  // nogncheck
#include "ui/events/base_event_utils.h"
#include "ui/events/gesture_detection/gesture_configuration.h"
#endif

#if BUILDFLAG(IS_LINUX)
#include "chrome/browser/ui/views/frame/browser_native_widget_aura_linux.h"
#include "chrome/common/chrome_features.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/linux/linux_ui.h"
#define DESKTOP_BROWSER_FRAME_AURA BrowserNativeWidgetAuraLinux
#else
#define DESKTOP_BROWSER_FRAME_AURA BrowserNativeWidgetAura
#endif

#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
#include "ui/ozone/public/ozone_platform.h"
#endif

#if BUILDFLAG(IS_WIN)
#include "ui/base/ui_base_features.h"
#endif

using content::WebContents;
using display::Display;
using ui_test_utils::GetDisplays;

namespace test {

namespace {

const char kTabDragControllerInteractiveUITestUserDataKey[] =
    "TabDragControllerInteractiveUITestUserData";

class FakeTabDragTarget : public TabDragTarget {
 public:
  FakeTabDragTarget() {
    drop_callback_ = base::BindRepeating([](DragController&) {});
  }
  ~FakeTabDragTarget() override = default;

  // TabDragTarget:
  TabDragContext* OnTabDragUpdated(TabDragTarget::DragController& controller,
                                   const gfx::Point& point_in_screen) override {
    drag_updated_ = true;
    return nullptr;
  }
  void OnTabDragEntered() override { drag_entered_ = true; }
  void OnTabDragExited(const gfx::Point& point_in_screen) override {
    drag_exited_ = true;
  }
  void OnTabDragEnded() override { drag_ended_ = true; }
  bool CanDropTab() override { return can_drop_; }
  void HandleTabDrop(DragController& controller) override {
    dropped_ = true;
    drop_callback_.Run(controller);
  }

  base::CallbackListSubscription RegisterWillDestroyCallback(
      base::OnceClosure callback) override {
    return subscription_list_.Add(std::move(callback));
  }

  bool drag_entered() const { return drag_entered_; }
  bool drag_exited() const { return drag_exited_; }
  bool dropped() const { return dropped_; }
  bool drag_ended() const { return drag_ended_; }
  bool drag_updated() const { return drag_updated_; }
  void set_can_drop(bool can_drop) { can_drop_ = can_drop; }
  void set_drop_callback(
      base::RepeatingCallback<void(DragController&)> drop_callback) {
    drop_callback_ = drop_callback;
  }

 private:
  bool drag_entered_ = false;
  bool drag_exited_ = false;
  bool dropped_ = false;
  bool drag_ended_ = false;
  bool drag_updated_ = false;
  bool can_drop_ = true;
  base::RepeatingCallback<void(DragController&)> drop_callback_;
  base::OnceCallbackList<void()> subscription_list_;
};

class FakeTabDragPointResolver : public TabDragPointResolver {
 public:
  explicit FakeTabDragPointResolver(FakeTabDragTarget* delegate)
      : delegate_(delegate) {}
  ~FakeTabDragPointResolver() override = default;

  TabDragTarget* GetDragTarget(BrowserView& browser_view,
                               const gfx::Point& point_in_screen) override {
    // Return the delegate only if the drag is outside the tabstrip.
    gfx::Rect tabstrip_bounds =
        GetTabStripForBrowser(browser_view.browser())->GetBoundsInScreen();
    if (tabstrip_bounds.Contains(point_in_screen)) {
      return nullptr;
    }
    return delegate_;
  }

 private:
  raw_ptr<FakeTabDragTarget> delegate_;
};

class TabDragControllerInteractiveUITestUserData
    : public base::SupportsUserData::Data {
 public:
  explicit TabDragControllerInteractiveUITestUserData(int id) : id_(id) {}
  ~TabDragControllerInteractiveUITestUserData() override = default;
  int id() { return id_; }

 private:
  int id_;
};

#if BUILDFLAG(IS_CHROMEOS)
aura::Window* GetWindowForTabStrip(TabStrip* tab_strip) {
  return tab_strip ? tab_strip->GetWidget()->GetNativeWindow() : nullptr;
}
#endif

gfx::Point GetLeftCenterInScreenCoordinates(const views::View* view) {
  gfx::Point center = view->GetLocalBounds().CenterPoint();
  center.set_x(center.x() - view->GetLocalBounds().width() / 4);
  views::View::ConvertPointToScreen(view, &center);
  return center;
}

gfx::Point GetRightCenterInScreenCoordinates(const views::View* view) {
  gfx::Point center = view->GetLocalBounds().CenterPoint();
  center.set_x(center.x() + view->GetLocalBounds().width() / 4);
  views::View::ConvertPointToScreen(view, &center);
  return center;
}

void WaitForBrowserActivation(BrowserWindowInterface* browser) {
  views::test::WaitForWidgetActive(
      BrowserView::GetBrowserViewForBrowser(browser)->GetWidget(), true);
  if (!browser->GetWindow()->IsActive()) {
    ADD_FAILURE() << "New browser window isn't active";
  }
}

bool IsDragSessionActive(TabStrip* tab_strip) {
  return tab_strip->GetDragContext()->GetDragController() != nullptr;
}

// `task` usually executes OS events (like `ReleaseInput()`) to
// end the drag. Because dragging on some platforms blocks in a nested
// RunLoop (`X11WholeScreenMoveLoop`), it must only execute after that
// loop has actively started.
// However, TabDragController may synchronously spin an internal
// `VisibilityWaiter` RunLoop before `RunMoveLoop()` begins. The first
// `PostTask` ensures we push a task into that `VisibilityWaiter`
// loop. The second nested `PostTask` ensures that *after* the
// visibility loop finishes, we queue the callback into the actual
// drag loop `RunMoveLoop` so it can successfully terminate the drag.
void PostTaskToRunMoveLoop(base::OnceClosure task) {
  base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
      FROM_HERE, base::BindOnce(
                     [](base::OnceClosure task) {
                       base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
                           FROM_HERE, std::move(task));
                     },
                     std::move(task)));
}

}  // namespace

// Note: do not use this if the code that ends the drag session runs after
// QuitDraggingObserver::Wait() returns. This will make the test time out when
// using system-DnD-based tab dragging due to the limitations of
// TabDragController::SetDragLoopDoneCallbackForTesting().
//
// If you need to wait for dragged tabs to be attached to another browser, use
// BrowserChangeWaiter instead.
class QuitDraggingObserver {
 public:
  explicit QuitDraggingObserver(TabStrip* tab_strip) {
    tab_strip->GetDragContext()->SetDragControllerCallbackForTesting(
        base::BindOnce(&QuitDraggingObserver::OnDragControllerSet,
                       weak_ptr_factory_.GetWeakPtr()));
  }

  QuitDraggingObserver(const QuitDraggingObserver&) = delete;
  QuitDraggingObserver& operator=(const QuitDraggingObserver&) = delete;
  ~QuitDraggingObserver() = default;

  // The observer should be constructed prior to initiating the drag. To prevent
  // misuse via constructing a temporary object, Wait is marked lvalue-only.
  void Wait() & {
    timeout_warning_timer_.Start(
        FROM_HERE, TestTimeouts::action_max_timeout(),
        base::BindLambdaForTesting([]() {
          LOG(ERROR) << "QuitDraggingObserver::Wait() is taking a long time. "
                        "If this test times out, please check the comment for "
                        "QuitDraggingObserver to see if it should be using "
                        "BrowserChangeWaiter instead.";
          LOG(ERROR) << "Note: you might be using QuitDraggingObserver via "
                        "DragTabAndNotify() or "
                        "DragToDetachGroupAndNotify().";
        }));
    run_loop_.Run();
    timeout_warning_timer_.Stop();
  }

 private:
  void OnDragControllerSet(TabDragController* controller) {
    controller->SetDragLoopDoneCallbackForTesting(base::BindOnce(
        &QuitDraggingObserver::Quit, weak_ptr_factory_.GetWeakPtr()));
  }

  void Quit() { run_loop_.QuitWhenIdle(); }

  base::RunLoop run_loop_;
  base::OneShotTimer timeout_warning_timer_;

  base::WeakPtrFactory<QuitDraggingObserver> weak_ptr_factory_{this};
};

// Wait for a browser be added or removed, e.g. when a new browser is created to
// hold dragged tabs or when such a browser is closed after attaching to another
// browser.
//
// This is similar to ui_test_utils::BrowserChangeObserver, but with the option
// to run a closure in response to a change. This is needed because we might
// have nested `base::RunLoop`s:
// - when using system-DnD-based tab dragging, the system DnD code will spawn a
//   base::RunLoop. If we create a new RunLoop for a waiter before starting the
//   drag, we can't exit the waiter's RunLoop before the drag ends.
// - when using regular tab dragging, we have the same problem, but the nested
//   RunLoop already exits when attaching, even if the mouse is not yet
//   released.
// Therefore, the closure that will be run when a browser is added/removed must
// ensure the following:
// - if using system-DnD-based tab dragging, that the system DnD session ends,
//   by releasing the mouse or cancelling the DnD session;
// - else, that the move loop ends, i.e. attaching to an existing browser or
//   fully ending the tab drag.
class BrowserChangeWaiter : public BrowserCollectionObserver {
 public:
  enum class ChangeType {
    kAdded,
    kRemoved,
  };

  explicit BrowserChangeWaiter(ChangeType type) : type_(type) {
    browser_collection_observation_.Observe(
        GlobalBrowserCollection::GetInstance());
  }
  BrowserChangeWaiter(const BrowserChangeWaiter&) = delete;
  BrowserChangeWaiter& operator=(const BrowserChangeWaiter&) = delete;
  ~BrowserChangeWaiter() override = default;

  // The closure must ensure the system DnD session/move loop ends (see comment
  // above).
  void Wait(base::OnceClosure closure) {
    closure_ = std::move(closure);
    run_loop_.Run();
  }

  // BrowserCollectionObserver:
  void OnBrowserCreated(BrowserWindowInterface* browser) override {
    if (type_ == ChangeType::kAdded) {
      Quit();
    }
  }

  void OnBrowserClosed(BrowserWindowInterface* browser) override {
    if (type_ == ChangeType::kRemoved) {
      Quit();
    }
  }

 private:
  void Quit() {
    if (quit_called_) {
      return;
    }
    // Browser addition/removal callbacks can be called multiple times, calling
    // `Quit()` each time.
    // So make sure that the `closure_` still gets to run if multiple `Quit()`
    // calls happen in quick succession.
    quit_called_ = true;
    if (closure_) {
      // For ChangeType::kRemoved, the browser is still closing and
      // synchronously running the closure now can lead to reentrancy issues,
      // so we instead PostTask() it. We also need to make sure we only quit
      // the RunLoop after the closure has run.
      // Use a double-PostTask to leverage SequencedTaskRunner FIFO ordering:
      // the first PostTask runs before SynchronouslyDestroyBrowser (enqueued
      // later in OnWindowClosing), and the second runs after it, ensuring the
      // closure executes after ~Browser() completes. See crbug.com/431671320.
      // It won't hurt to use the same approach for ChangeType::kAdded.
      base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
          FROM_HERE,
          base::BindOnce(
              [](base::OnceClosure closure, base::OnceClosure quit_closure) {
                base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
                    FROM_HERE,
                    std::move(closure).Then(std::move(quit_closure)));
              },
              std::move(closure_), run_loop_.QuitClosure()));
    } else {
      run_loop_.Quit();
    }
  }

  ChangeType type_;
  bool quit_called_ = false;
  base::OnceClosure closure_;
  base::RunLoop run_loop_{base::RunLoop::Type::kNestableTasksAllowed};
  base::ScopedObservation<GlobalBrowserCollection, BrowserCollectionObserver>
      browser_collection_observation_{this};
};

void SetID(WebContents* web_contents, int id) {
  web_contents->SetUserData(
      &kTabDragControllerInteractiveUITestUserDataKey,
      std::make_unique<TabDragControllerInteractiveUITestUserData>(id));
}

void ResetIDs(TabStripModel* model, int start) {
  for (int i = 0; i < model->count(); ++i) {
    SetID(model->GetWebContentsAt(i), start + i);
  }
}

std::string IDString(TabStripModel* model) {
  std::string result;
  for (int i = 0; i < model->count(); ++i) {
    if (i != 0) {
      result += " ";
    }
    WebContents* contents = model->GetWebContentsAt(i);
    TabDragControllerInteractiveUITestUserData* user_data =
        static_cast<TabDragControllerInteractiveUITestUserData*>(
            contents->GetUserData(
                &kTabDragControllerInteractiveUITestUserDataKey));
    if (user_data) {
      result += base::NumberToString(user_data->id());
    } else {
      result += "?";
    }
  }
  return result;
}

TabStrip* GetTabStripForBrowser(BrowserWindowInterface* browser) {
  return BrowserView::GetBrowserViewForBrowser(browser)
      ->horizontal_tab_strip_for_testing();
}

TabDragController* GetTabDragController(TabStrip* tab_strip) {
  return tab_strip->GetDragContext()->GetDragController();
}

// Resizes the given browser to the specified bounds by using ui_test_utils to
// generate mouse movements, similar to how a regular user would resize the
// window. This is used instead of BrowserWindow::SetBounds() on platforms where
// clients don't have complete control over window bounds (i.e., Wayland).
void ResizeUsingMouseEmulation(BrowserWindowInterface* browser,
                               const gfx::Rect& target_bounds) {
#if BUILDFLAG(IS_LINUX)
  auto* window = browser->GetWindow()->GetNativeWindow();
  auto width_difference = window->bounds().width() - target_bounds.width();
  auto height_difference = window->bounds().height() - target_bounds.height();

  // Resize the window, if needed.
  if (width_difference != 0 || height_difference != 0) {
    // We use the container bounds because they don't include window
    // decorations.
    auto bottom_right = browser->GetTabStripModel()
                            ->GetActiveWebContents()
                            ->GetContainerBounds()
                            .bottom_right();
    auto resize_target = gfx::Point(bottom_right.x() - width_difference,
                                    bottom_right.y() - height_difference);
    ASSERT_TRUE(ui_test_utils::SendMouseMoveSync(bottom_right, window));
    ASSERT_TRUE(ui_test_utils::SendMouseEventsSync(
        ui_controls::MouseButton::LEFT, ui_controls::MouseButtonState::DOWN,
        window));
    ASSERT_TRUE(ui_test_utils::SendMouseMoveSync(resize_target, window));
    ASSERT_TRUE(ui_test_utils::SendMouseEventsSync(
        ui_controls::MouseButton::LEFT, ui_controls::MouseButtonState::UP,
        window));
  }

  // Move the window.
  auto* grab_handle_space =
      BrowserElementsViews::From(browser)->GetViewAs<views::View>(
          kTabStripFrameGrabHandleElementId);
  auto grab_coordinates =
      ui_test_utils::GetCenterInScreenCoordinates(grab_handle_space);
  gfx::Rect window_bounds = browser->GetWindow()->GetBounds();
  gfx::Vector2d grab_offset = grab_coordinates - window_bounds.origin();
  auto move_target = target_bounds.origin() + grab_offset;
  ASSERT_TRUE(ui_test_utils::SendMouseMoveSync(grab_coordinates, window));
  ASSERT_TRUE(ui_test_utils::SendMouseEventsSync(
      ui_controls::MouseButton::LEFT, ui_controls::MouseButtonState::DOWN,
      window));

  // Move the mouse past the drag threshold to allow the real move to proceed.
  int threshold = ui::LinuxUi::kDefaultWindowDragThreshold + 1;
  if (auto* linux_ui = ui::LinuxUi::instance()) {
    threshold = linux_ui->GetWindowDragThresholdPx() + 1;
  }
  auto drag_threshold_move_target =
      grab_coordinates + gfx::Vector2d(threshold, threshold);
  ui_controls::ForceUseScreenCoordinatesOnce();
  ASSERT_TRUE(
      ui_test_utils::SendMouseMoveSync(drag_threshold_move_target, window));

  // `move_target` is in screen coordinates.
  ui_controls::ForceUseScreenCoordinatesOnce();
  ASSERT_TRUE(ui_test_utils::SendMouseMoveSync(move_target, window));

  ASSERT_TRUE(ui_test_utils::SendMouseEventsSync(
      ui_controls::MouseButton::LEFT, ui_controls::MouseButtonState::UP,
      window));
#endif  // BUILDFLAG(IS_LINUX)
}

// Returns a point within `target_bounds` to drag to, near `preferred_x` but
// right of `source_bounds`, which can overlap the target on small screens.
gfx::Point DragPointInTabStrip(const gfx::Rect& target_bounds,
                               const gfx::Rect& source_bounds,
                               int preferred_x) {
  int x = std::max(preferred_x, source_bounds.right() + 1);
  x = std::min(x, target_bounds.right() - 1);
  x = std::max(x, target_bounds.x());
  return gfx::Point(x, target_bounds.CenterPoint().y());
}

// If this returns false, we must use ResizeUsingMouseEmulation() instead of
// BrowserWindow::SetBounds().
bool PlatformSupportsScreenCoordinates() {
#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
  return ui::OzonePlatform::GetInstance()
      ->GetPlatformProperties()
      .supports_global_screen_coordinates;
#else
  return true;
#endif  // BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
}

}  // namespace test

using test::GetTabDragController;
using test::GetTabStripForBrowser;
using test::IDString;
using test::IsDragSessionActive;
using test::ResetIDs;
using test::SetID;
using test::WaitForBrowserActivation;
using ui_test_utils::GetCenterInScreenCoordinates;

TabDragControllerTest::TabDragControllerTest() = default;

TabDragControllerTest::~TabDragControllerTest() = default;

void TabDragControllerTest::StopAnimating(TabStrip* tab_strip) {
  tab_strip->StopAnimating();
}

void TabDragControllerTest::AddTabsAndResetBrowser(
    BrowserWindowInterface* browser,
    int additional_tabs,
    const GURL& url) {
  for (int i = 0; i < additional_tabs; i++) {
    auto* contents = chrome::AddSelectedTabWithURL(
        browser, url, ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
    content::WaitForLoadStop(contents);
  }
  browser->GetWindow()->Show();
  StopAnimating(GetTabStripForBrowser(browser));
  // Perform any scheduled layouts so the tabstrip is in a steady state.
  BrowserView::GetBrowserViewForBrowser(browser)
      ->GetWidget()
      ->LayoutRootViewIfNecessary();
  ResetIDs(browser->GetTabStripModel(), 0);
}

BrowserWindowInterface* TabDragControllerTest::CreateAnotherBrowserAndResize() {
  // Resize the two windows so they're right next to each other.

  // If we're using test::ResizeUsingMouseEmulation(), it's important we resize
  // the first browser before creating the second one, else the second browser
  // might occlude the first browser's bottom right corner or tab strip grab
  // handle space, preventing us from resizing or moving it.
  const gfx::NativeWindow window = browser()->GetWindow()->GetNativeWindow();
  gfx::Rect work_area =
      display::Screen::Get()->GetDisplayNearestWindow(window).work_area();
  const gfx::Size size(work_area.width() / 3, work_area.height() / 2);
  gfx::Rect browser_rect(work_area.origin() + gfx::Vector2d(50, 50), size);

  if (test::PlatformSupportsScreenCoordinates()) {
    ui_test_utils::SetAndWaitForBounds(*browser(), browser_rect);
    browser_rect.set_x(browser_rect.right());
  } else {
    test::ResizeUsingMouseEmulation(browser(), browser_rect);

    // `container_bounds` doesn't include window decorations, so we can use it
    // to calculate the window decorations' width by comparing its offset from
    // the browser window's bounds.
    auto container_bounds = browser()
                                ->GetTabStripModel()
                                ->GetActiveWebContents()
                                ->GetContainerBounds();
    auto window_decoration_width =
        container_bounds.x() - browser()->GetWindow()->GetBounds().x();
    // We need to correct for the decorations drawn to the right of the first
    // window and to the left of the second window.
    browser_rect.set_x(browser_rect.right() - 2 * window_decoration_width);
  }

  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  ResetIDs(browser2->GetTabStripModel(), 100);
  if (test::PlatformSupportsScreenCoordinates()) {
    ui_test_utils::SetAndWaitForBounds(*browser2, browser_rect);
  } else {
    test::ResizeUsingMouseEmulation(browser2, browser_rect);
  }
  BrowserView::GetBrowserViewForBrowser(browser2)
      ->GetWidget()
      ->LayoutRootViewIfNecessary();
  // Also flush the source browser, resized above: SetAndWaitForBounds() waits
  // for the bounds change, not the layout it schedules.
  BrowserView::GetBrowserViewForBrowser(browser())
      ->GetWidget()
      ->LayoutRootViewIfNecessary();
  return browser2;
}

void TabDragControllerTest::SetWindowFinderForTabStrip(
    TabStrip* tab_strip,
    std::unique_ptr<WindowFinder> window_finder) {
  ASSERT_TRUE(GetTabDragController(tab_strip));
  GetTabDragController(tab_strip)->window_finder_ = std::move(window_finder);
}

void TabDragControllerTest::HandleGestureEvent(TabStrip* tab_strip,
                                               ui::GestureEvent* event) {
  // Manually dispatch the event to the drag context (which has capture during
  // drags). Mouse/keyboard/touch events can be dispatched normally via test
  // machinery in ui_test_utils that have no direct gesture event equivalent.
  tab_strip->GetDragContext()->OnGestureEvent(event);
}

bool TabDragControllerTest::HasDragStarted(TabStrip* tab_strip) const {
  return GetTabDragController(tab_strip) &&
         GetTabDragController(tab_strip)->started_drag();
}

bool TabDragControllerTest::IsWaitingForWindowToShow(
    TabStrip* tab_strip) const {
  TabDragController* controller = GetTabDragController(tab_strip);
  return controller &&
         controller->current_state_ ==
             TabDragController::DragState::kWaitingForWindowToShow;
}

void TabDragControllerTest::SetTabDragPointResolver(
    TabDragPointResolver& resolver) {
  TabDragController::SetTabDragPointResolver(resolver);
}

void TabDragControllerTest::SetUp() {
#if defined(USE_AURA)
  // This needs to be disabled as it can interfere with when events are
  // processed. In particular if input throttling is turned on, then when an
  // event ack runs the event may not have been processed.
  aura::Env::set_initial_throttle_input_on_resize_for_testing(false);
#endif
  InProcessBrowserTest::SetUp();
}

namespace {

int GetDetachY(TabStrip* tab_strip) {
  return std::max(TabDragController::kTouchVerticalDetachMagnetism,
                  TabDragController::kVerticalDetachMagnetism) +
         tab_strip->height() + 1;
}

bool GetIsDragged(BrowserWindowInterface* browser) {
#if BUILDFLAG(IS_CHROMEOS)
  return ash::WindowState::Get(browser->GetWindow()->GetNativeWindow())
      ->is_dragged();
#else
  return false;
#endif
}

}  // namespace

#if !BUILDFLAG(IS_CHROMEOS) && defined(USE_AURA)

// Following classes verify a crash scenario. Specifically on Windows when focus
// changes it can trigger capture being lost. This was causing a crash in tab
// dragging as it wasn't set up to handle this scenario. These classes
// synthesize this scenario.

// Allows making ClearNativeFocus() invoke ReleaseCapture().
class TestDesktopBrowserFrameAura : public DESKTOP_BROWSER_FRAME_AURA {
 public:
  TestDesktopBrowserFrameAura(BrowserWidget* browser_widget,
                              BrowserView* browser_view)
      : DESKTOP_BROWSER_FRAME_AURA(browser_widget, browser_view) {}
  TestDesktopBrowserFrameAura(const TestDesktopBrowserFrameAura&) = delete;
  TestDesktopBrowserFrameAura& operator=(const TestDesktopBrowserFrameAura&) =
      delete;
  ~TestDesktopBrowserFrameAura() override = default;

  void ReleaseCaptureOnNextClear() { release_capture_ = true; }

  void ClearNativeFocus() override {
    views::DesktopNativeWidgetAura::ClearNativeFocus();
    if (release_capture_) {
      release_capture_ = false;
      GetWidget()->ReleaseCapture();
    }
  }

 private:
  // If true ReleaseCapture() is invoked in ClearNativeFocus().
  bool release_capture_ = false;
};

// Factory for creating a TestDesktopBrowserFrameAura.
class TestBrowserNativeWidgetFactory : public BrowserNativeWidgetFactory {
 public:
  TestBrowserNativeWidgetFactory() = default;
  TestBrowserNativeWidgetFactory(const TestBrowserNativeWidgetFactory&) =
      delete;
  TestBrowserNativeWidgetFactory& operator=(
      const TestBrowserNativeWidgetFactory&) = delete;
  ~TestBrowserNativeWidgetFactory() override = default;

  BrowserNativeWidget* Create(BrowserWidget* browser_widget,
                              BrowserView* browser_view) override {
    return new TestDesktopBrowserFrameAura(browser_widget, browser_view);
  }
};

class TabDragCaptureLostTest : public TabDragControllerTest {
 public:
  TabDragCaptureLostTest() {
    BrowserNativeWidgetFactory::Set(new TestBrowserNativeWidgetFactory);
  }
  TabDragCaptureLostTest(const TabDragCaptureLostTest&) = delete;
  TabDragCaptureLostTest& operator=(const TabDragCaptureLostTest&) = delete;
};

// See description above for details.
IN_PROC_BROWSER_TEST_F(TabDragCaptureLostTest, ReleaseCaptureOnDrag) {
  AddTabsAndResetBrowser(browser(), 1);

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  gfx::Point tab_1_center(GetCenterInScreenCoordinates(tab_strip->tab_at(1)));
  ASSERT_TRUE(
      ui_test_utils::SendMouseMoveSync(tab_1_center) &&
      ui_test_utils::SendMouseEventsSync(ui_controls::LEFT, ui_controls::DOWN));
  TestDesktopBrowserFrameAura* frame =
      static_cast<TestDesktopBrowserFrameAura*>(
          BrowserView::GetBrowserViewForBrowser(browser())
              ->GetWidget()
              ->native_widget_private());
  // Invoke ReleaseCaptureOnDrag() so that when the drag happens and focus
  // changes capture is released and the drag cancels.
  frame->ReleaseCaptureOnNextClear();
  ASSERT_TRUE(ui_test_utils::SendMouseMoveSync(
      GetCenterInScreenCoordinates(tab_strip->tab_at(0))));
  EXPECT_FALSE(IsDragSessionActive(tab_strip));
}

#endif

IN_PROC_BROWSER_TEST_F(TabDragControllerTest, GestureEndShouldEndDragTest) {
  AddTabsAndResetBrowser(browser(), 1);

  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  Tab* tab1 = tab_strip->tab_at(1);
  gfx::Point tab_1_center(tab1->width() / 2, tab1->height() / 2);

  ui::GestureEvent gesture_tap_down(
      tab_1_center.x(), tab_1_center.x(), 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::EventType::kGestureTapDown));
  tab_strip->MaybeStartDrag(tab1, gesture_tap_down,
                            tab_strip->GetSelectionModel());
  EXPECT_TRUE(TabDragController::IsActive());

  ui::GestureEvent gesture_end(
      tab_1_center.x(), tab_1_center.x(), 0, base::TimeTicks(),
      ui::GestureEventDetails(ui::EventType::kGestureEnd));
  HandleGestureEvent(tab_strip, &gesture_end);
  EXPECT_FALSE(TabDragController::IsActive());
  EXPECT_FALSE(IsDragSessionActive(tab_strip));
}

class DetachToBrowserTabDragControllerTest
    : public TabDragControllerInteractiveTestMixin<TabDragControllerTest>,
      public ::testing::WithParamInterface<testing::tuple<bool, const char*>> {
 public:
  DetachToBrowserTabDragControllerTest() {
    std::vector<base::test::FeatureRefAndParams> enabled_features_with_params;

    std::vector<base::test::FeatureRef> disabled_features;

#if BUILDFLAG(IS_WIN)
    // Disable NativeWinOcclusion to avoid it interfering with test for dragging
    // over occluded browser window.
    disabled_features.push_back(features::kCalculateNativeWinOcclusion);
#endif  // BUILDFLAG(IS_WIN)
    if (std::get<0>(GetParam())) {
      enabled_features_with_params.push_back(
          {features::kTearOffWebAppTabOpensWebAppWindow, {}});
    }
    scoped_feature_list_.InitWithFeaturesAndParameters(
        enabled_features_with_params, disabled_features);
  }
  DetachToBrowserTabDragControllerTest(
      const DetachToBrowserTabDragControllerTest&) = delete;
  DetachToBrowserTabDragControllerTest& operator=(
      const DetachToBrowserTabDragControllerTest&) = delete;

  void SetUpOnMainThread() override {
    TabDragControllerTest::SetUpOnMainThread();
#if BUILDFLAG(IS_CHROMEOS)
    root_ = browser()->GetWindow()->GetNativeWindow()->GetRootWindow();

    // Disable flings which might otherwise inadvertently be generated from
    // tests' touch events.
    SetMinFlingVelocity(std::numeric_limits<float>::max());
#endif
#if BUILDFLAG(IS_MAC)
    // Currently MacViews' browser windows are shown in the background and could
    // be obscured by other windows if there are any. This should be fixed in
    // order to be consistent with other platforms.
    EXPECT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
#endif  // BUILDFLAG(IS_MAC)
        // Disable Split View drag and drop to avoid creating Split Views.
    browser()->GetProfile()->GetPrefs()->SetBoolean(
        prefs::kSplitViewDragAndDropEnabled, false);
  }

  InputSource input_source() override {
    return UNSAFE_TODO(strstr(std::get<1>(GetParam()), "mouse"))
               ? InputSource::INPUT_SOURCE_MOUSE
               : InputSource::INPUT_SOURCE_TOUCH;
  }

  bool DragInputToAsync(const gfx::Point& location,
                        const gfx::NativeWindow window_hint) {
    if (input_source() == InputSource::INPUT_SOURCE_MOUSE) {
      return ui_controls::SendMouseMove(location.x(), location.y(),
                                        window_hint);
    }
#if BUILDFLAG(IS_CHROMEOS)
    return ui_controls::SendTouchEvents(ui_controls::kTouchMove, 0,
                                        location.x(), location.y());
#else
    NOTREACHED();
#endif
  }

  // Like DragInputToCenter(), but for DragInputToAsync() instead of
  // DragInputTo().
  bool DragInputToCenterAsync(const views::View* view,
                              gfx::Vector2d offset = {}) {
    return DragInputToAsync(GetCenterInScreenCoordinates(view) + offset,
                            GetWindowHint(view));
  }

  bool DragInputToNotifyWhenDone(const gfx::Point& location,
                                 base::OnceClosure task,
                                 const gfx::NativeWindow window_hint) {
    if (input_source() == InputSource::INPUT_SOURCE_MOUSE) {
      return ui_controls::SendMouseMoveNotifyWhenDone(
          location.x(), location.y(), std::move(task), window_hint);
    }

#if BUILDFLAG(IS_CHROMEOS)
    return ui_controls::SendTouchEventsNotifyWhenDone(
        ui_controls::kTouchMove, 0, location.x(), location.y(),
        std::move(task));
#else
    NOTREACHED();
#endif
  }

  // Like DragInputToCenter(), but for DragInputToCenterNotifyWhenDone() instead
  // of DragInputTo().
  bool DragInputToCenterNotifyWhenDone(const views::View* view,
                                       base::OnceClosure task,
                                       gfx::Vector2d offset = {}) {
    gfx::Point location = GetCenterInScreenCoordinates(view) + offset;
    return DragInputToNotifyWhenDone(location, std::move(task),
                                     GetWindowHint(view));
  }

  void ReleaseInputAfterWindowDetached(
      int first_dragged_tab_width,
      BrowserWindowInterface* source_browser,
      BrowserWindowInterface* detached_browser) {
    // Only check tab width if dragging tabs between windows of the same type.
    if (source_browser->GetType() == detached_browser->GetType()) {
      // The tab getting dragged into the new browser should have the same
      // width as before it was dragged.
      EXPECT_EQ(first_dragged_tab_width,
                GetTabStripForBrowser(detached_browser)->tab_at(0)->width());
    }
    // Windows hangs if you use a sync mouse event here.
    ASSERT_TRUE(ReleaseInput(0, true));
  }

  bool MoveInputTo(const gfx::Point& location) {
    aura::Env::GetInstance()->SetLastMouseLocation(location);
    if (input_source() == InputSource::INPUT_SOURCE_MOUSE) {
      return ui_test_utils::SendMouseMoveSync(location);
    }
#if BUILDFLAG(IS_CHROMEOS)
    return SendTouchEventsSync(ui_controls::kTouchMove, 0, location);
#else
    NOTREACHED();
#endif
  }

  void AddBlankTabAndShow(BrowserWindowInterface* browser) {
    InProcessBrowserTest::AddBlankTabAndShow(browser);
  }

  // Returns true if the tab dragging info is correctly set on the attached
  // browser window.
  bool IsTabDraggingInfoSet(TabStrip* attached_tabstrip) {
    DCHECK(attached_tabstrip);
#if BUILDFLAG(IS_CHROMEOS)
    aura::Window* dragged_window =
        test::GetWindowForTabStrip(attached_tabstrip);
    attached_tabstrip->GetWidget()->GetNativeWindow();
    return dragged_window->GetProperty(ash::kIsDraggingTabsKey);
#else
    return true;
#endif
  }

  // Returns true if the tab dragging info is correctly cleared on the attached
  // browser window.
  bool IsTabDraggingInfoCleared(TabStrip* attached_tabstrip) {
    DCHECK(attached_tabstrip);
#if BUILDFLAG(IS_CHROMEOS)
    aura::Window* dragged_window =
        test::GetWindowForTabStrip(attached_tabstrip);
    return !dragged_window->GetProperty(ash::kIsDraggingTabsKey) &&
           !dragged_window->GetProperty(ash::kTabDraggingSourceWindowKey);
#else
    return true;
#endif
  }

  // Drags a tab in `source_browser` such that it detaches and creates a new
  // window. Returns the resulting detached browser.
  BrowserWindowInterface* DragTabForDetachAndNotify(
      BrowserWindowInterface* source_browser,
      base::OnceCallback<void(BrowserWindowInterface*, BrowserWindowInterface*)>
          did_drag_cb,
      int tab_index = 0,
      int drag_x_offset = 0) {
    TabStrip* const source_tab_strip = GetTabStripForBrowser(source_browser);
    test::QuitDraggingObserver observer(source_tab_strip);

    // Move to the tab and drag it enough so that it detaches.
    Tab* const tab = source_tab_strip->tab_at(tab_index);
    EXPECT_TRUE(PressInputAtCenter(tab));

    BrowserWindowInterface* detached_browser = nullptr;
    ui_test_utils::BrowserCreatedObserver browser_created_observer;
    EXPECT_TRUE(DragInputToCenterNotifyWhenDone(
        tab, base::BindLambdaForTesting([&]() {
          // Once the drag is complete, wait for the detached browser.
          detached_browser = browser_created_observer.Wait();

          test::PostTaskToRunMoveLoop(base::BindLambdaForTesting([&]() {
            std::move(did_drag_cb).Run(source_browser, detached_browser);
          }));
        }),
        gfx::Vector2d(drag_x_offset, GetDetachY(source_tab_strip))));
    observer.Wait();

    return detached_browser;
  }

  // Continues an active drag session to detach a tab and creates a new window.
  // Returns the resulting detached browser. This is similar to
  // `DragTabForDetachAndNotify` but does not call `PressInputAtCenter`,
  // assuming the drag is already in progress. It uses `base::test::RunUntil`
  // to wait for the drag session to end, as `QuitDraggingObserver` may not
  // work reliably for continuing drags.
  BrowserWindowInterface* ContinueDragTabForDetachAndNotify(
      BrowserWindowInterface* source_browser,
      base::OnceCallback<void(BrowserWindowInterface*, BrowserWindowInterface*)>
          did_drag_cb,
      int tab_index = 0,
      int drag_x_offset = 0) {
    TabStrip* const source_tab_strip = GetTabStripForBrowser(source_browser);

    BrowserWindowInterface* detached_browser = nullptr;
    ui_test_utils::BrowserCreatedObserver browser_created_observer;
    EXPECT_TRUE(DragInputToCenterNotifyWhenDone(
        source_tab_strip->tab_at(tab_index), base::BindLambdaForTesting([&]() {
          detached_browser = browser_created_observer.Wait();
          test::PostTaskToRunMoveLoop(base::BindLambdaForTesting([&]() {
            std::move(did_drag_cb).Run(source_browser, detached_browser);
          }));
        }),
        gfx::Vector2d(drag_x_offset, GetDetachY(source_tab_strip))));

    EXPECT_TRUE(
        base::test::RunUntil([&]() { return !TabDragController::IsActive(); }));

    return detached_browser;
  }

  void DragTabAndNotify(TabStrip* tab_strip,
                        base::OnceClosure task,
                        int tab_index = 0,
                        int drag_x_offset = 0) {
    test::QuitDraggingObserver observer(tab_strip);
    // Move to the tab and drag it enough so that it detaches.
    Tab* tab = tab_strip->tab_at(tab_index);
    ASSERT_TRUE(PressInputAtCenter(tab));
    ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
        tab, base::BindLambdaForTesting([task = std::move(task)]() mutable {
          test::PostTaskToRunMoveLoop(std::move(task));
        }),
        gfx::Vector2d(drag_x_offset, GetDetachY(tab_strip))));
    observer.Wait();
  }

  void DragToDetachGroupAndNotify(TabStrip* tab_strip,
                                  base::OnceClosure task,
                                  tab_groups::TabGroupId group,
                                  int drag_x_offset = 0) {
    test::QuitDraggingObserver observer(tab_strip);
    // Move to the tab and drag it enough so that it detaches.
    views::View* group_header = tab_strip->group_header(group);
    ASSERT_TRUE(PressInputAtCenter(group_header));
    ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
        group_header,
        base::BindLambdaForTesting([task = std::move(task)]() mutable {
          test::PostTaskToRunMoveLoop(std::move(task));
        }),
        gfx::Vector2d(drag_x_offset, GetDetachY(tab_strip))));
    observer.Wait();
  }

  // Executes `task` once the `new_browser` exists in the BrowserCollection and
  // its widget is visible. Effectively guarantees execution safely after
  // TabDragController's VisibilityWaiter has exited. A generic asynchronous
  // browser waiter that intercepts OnBrowserCreated and queues a
  // double-PostTask before RunMoveLoop() spins up.
  class AsyncBrowserWaiter : public BrowserCollectionObserver {
   public:
    explicit AsyncBrowserWaiter(
        base::OnceCallback<void(BrowserWindowInterface*)> callback)
        : callback_(std::move(callback)) {
      browser_collection_observation_.Observe(
          GlobalBrowserCollection::GetInstance());
    }
    ~AsyncBrowserWaiter() override = default;

    void OnBrowserCreated(BrowserWindowInterface* browser) override {
      browser_collection_observation_.Reset();
      test::PostTaskToRunMoveLoop(base::BindOnce(
          [](base::WeakPtr<AsyncBrowserWaiter> w, BrowserWindowInterface* b) {
            if (w && w->callback_) {
              std::move(w->callback_).Run(b);
            }
          },
          weak_ptr_factory_.GetWeakPtr(), browser));
    }

   private:
    base::OnceCallback<void(BrowserWindowInterface*)> callback_;
    base::ScopedObservation<GlobalBrowserCollection, BrowserCollectionObserver>
        browser_collection_observation_{this};
    base::WeakPtrFactory<AsyncBrowserWaiter> weak_ptr_factory_{this};
  };

  // Waits for the drag to attach to `target_tab_strip` with
  // `expected_tab_count` tabs, then settles the attach animation. Call before
  // indexing into the target: the drag input that triggers the attach is sent
  // asynchronously, so the model may still be pre-attach when the move loop's
  // done-callback returns.
  [[nodiscard]] bool WaitForAttach(TabStrip* target_tab_strip,
                                   int expected_tab_count) {
    if (!base::test::RunUntil([&]() {
          return IsDragSessionActive(target_tab_strip) &&
                 target_tab_strip->GetTabCount() == expected_tab_count;
        })) {
      return false;
    }

    StopAnimating(target_tab_strip);
    return true;
  }

  // Helper method to click the first tab. Used to ensure no additional widgets
  // are in focus. For example, the tab editor bubble  is automatically opened
  // upon creating a new group.
  void EnsureFocusToTabStrip(TabStrip* tab_strip) {
    ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
    ASSERT_TRUE(ReleaseInput());
  }

  Browser* browser() const { return InProcessBrowserTest::browser(); }

 protected:
#if BUILDFLAG(IS_CHROMEOS)
  void SetMinFlingVelocity(float velocity) {
    ui::GestureConfiguration::GetInstance()->set_min_fling_velocity(velocity);
  }

  // The root window for the event generator.
  raw_ptr<aura::Window, DanglingUntriaged> root_ = nullptr;
#endif

 private:
  base::test::ScopedFeatureList scoped_feature_list_;
  std::optional<webapps::AppId> tabbed_app_id_;

  // Some of these tests rely on animation being enabled. This forces
  // animation on even if it's turned off in the OS.
  gfx::AnimationTestApi::RenderModeResetter animation_mode_reset_{
      gfx::AnimationTestApi::SetRichAnimationRenderMode(
          gfx::Animation::RichAnimationRenderMode::FORCE_ENABLED)};
};

// Creates a browser with four tabs. The first three belong in the same Tab
// Group. Dragging the third tab to after the fourth tab will result in a
// removal of the dragged tab from its group. Then dragging the second tab to
// after the third tab will also result in a removal of that dragged tab from
// its group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragRightToUngroupTab) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({0, 1, 2});
  StopAnimating(tab_strip);

  EXPECT_EQ(4, model->count());
  EXPECT_EQ(3u, model->group_model()->GetTabGroup(group)->ListTabs().length());

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(3)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Dragging the tab in the second index to the tab in the third index switches
  // the tabs and removes the dragged tab from the group.
  EXPECT_EQ("0 1 3 2", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(0, 2));
  EXPECT_EQ(std::nullopt, model->GetTabGroupForTab(2));
  EXPECT_EQ(std::nullopt, model->GetTabGroupForTab(3));

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Dragging the tab in the first index to the tab in the second index (tab 3)
  // switches the tabs and removes the dragged tab from the group.
  EXPECT_EQ("0 3 1 2", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(0, 1));
  EXPECT_EQ(std::nullopt, model->GetTabGroupForTab(1));
}

// Creates a browser with four tabs. The last three belong in the same Tab
// Group. Dragging the second tab to before the first tab will result in a
// removal of the dragged tab from its group. Then dragging the third tab to
// before the second tab will also result in a removal of that dragged tab from
// its group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragLeftToUngroupTab) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({1, 2, 3});
  StopAnimating(tab_strip);

  EXPECT_EQ(4, model->count());
  EXPECT_EQ(3u, model->group_model()->GetTabGroup(group)->ListTabs().length());
  EXPECT_EQ(group, model->GetTabGroupForTab(1).value());

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Dragging the tab in the first index to the tab in the zero-th index
  // switches the tabs and removes the dragged tab from the group.
  EXPECT_EQ("1 0 2 3", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(2, 4));
  EXPECT_EQ(std::nullopt, model->GetTabGroupForTab(0));
  EXPECT_EQ(std::nullopt, model->GetTabGroupForTab(1));

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Dragging the tab in the second index to the tab in the first index switches
  // the tabs and removes the dragged tab from the group.
  EXPECT_EQ("1 2 0 3", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(3, 4));
  EXPECT_EQ(std::nullopt, model->GetTabGroupForTab(2));
}

// Creates a browser with four tabs. The first three belong in the same Tab
// Group. Dragging tabs in a tab group within the defined threshold does not
// modify the group of the dragged tab.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragTabWithinGroupDoesNotModifyGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({0, 1, 2});
  StopAnimating(tab_strip);

  EXPECT_EQ(4, model->count());
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(0, 3));

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Dragging the tab in the zero-th index to the tab in the first index
  // switches the tabs but group membership does not change.
  EXPECT_EQ("1 0 2 3", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(0, 3));

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Dragging the tab in the second index to the tab in the first index switches
  // the tabs but group membership does not change.
  EXPECT_EQ("1 2 0 3", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(0, 3));
}

// Creates a browser with four tabs. The first tab is in a Tab Group. Dragging
// the only tab in that group will retain the group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragOnlyTabInGroupRetainsGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({0});
  StopAnimating(tab_strip);

  EXPECT_EQ(4, model->count());
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(0, 1));

  // Dragging the tab in the zero-th index to the tab in the first index
  // switches the tabs and removes the group of the zero-th tab.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("1 0 2 3", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(1, 2));
}

// Creates a browser with four tabs. The first tab is in Tab Group 1. The
// fourth tab is in Tab Group 2. Dragging the second tab over one to the left
// will result in the second tab joining Tab Group 1. Then dragging the third
// tab over one to the left will result in the tab joining Tab Group 1. Then
// dragging the fourth tab over one to the left will not result in the tab
// joining Tab Group 1 as well as it is the only tab in its group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragSingleTabLeftIntoGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group1 = model->AddToNewGroup({0});
  tab_groups::TabGroupId group4 = model->AddToNewGroup({3});
  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  // Dragging the tab in the first index toward the tab in the zero-th index
  // switches the tabs and adds the dragged tab to the group.
  // Drag only half the tab length, to ensure that the tab in the first index
  // lands in the slot between the tab in the zero-th index and the group
  // header to the left of the zero-th index.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(
      DragInputToCenter(tab_strip->tab_at(1),
                        gfx::Vector2d(-tab_strip->tab_at(0)->width() / 2, 0)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("1 0 2 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(0, 2));

  // Dragging the tab in the second index to the tab in the first index switches
  // the tabs and adds the dragged tab to the group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("1 2 0 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(0, 3));

  // Dragging the tab in the third index to the tab in the second index switches
  // the tabs and adds the dragged tab to the group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(3)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("1 2 0 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(0, 3));
  EXPECT_EQ(group_model->GetTabGroup(group4)->ListTabs(), gfx::Range(3, 4));
}

// Creates a browser with five tabs. The fourth tab is in Tab Group 1. The
// first tab is in Tab Group 2. Dragging the third tab over one to the right
// will result in the tab joining Tab Group 1. Then dragging the second tab
// over one to the right will result in the tab joining Tab Group 1. Then
// dragging the first tab over one to the right will not result in the tab
// joining Tab Group 1.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragSingleTabRightIntoGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 4);
  tab_groups::TabGroupId group1 = model->AddToNewGroup({3});
  tab_groups::TabGroupId group2 = model->AddToNewGroup({0});
  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  // Dragging the tab in the second index to the tab in the third index switches
  // the tabs and adds the dragged tab to the group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(3)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("0 1 3 2 4", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(2, 4));

  // Dragging the tab in the first index to the tab in the second index switches
  // the tabs and adds the dragged tab to the group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("0 3 1 2 4", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(1, 4));

  // Dragging the tab in the zero-th index to the tab in the first index
  // switches the tabs and adds the dragged tab to the group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("0 3 1 2 4", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(1, 4));
  EXPECT_EQ(group_model->GetTabGroup(group2)->ListTabs(), gfx::Range(0, 1));
}

// Creates a browser with five tabs. The last two tabs are in a Tab Group.
// Dragging the first tab past the last slot should allow it to exit the group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragSingleTabRightOfRightmostGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 4);
  tab_groups::TabGroupId group = model->AddToNewGroup({3, 4});
  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  // Dragging the first tab past the last slot brings it to the end of the
  // tabstrip, where it should not be in a group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(4), gfx::Vector2d(1, 0)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("1 2 3 4 0", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group)->ListTabs(), gfx::Range(2, 4));
}

// Creates a browser with four tabs. Second and Fourth tabs are parts of their
// own groups. Selecting and dragging the first and third tabs right at the
// first tab will result in the tabs joining the same group as the tab in the
// second position. Then dragging the tabs over two to the right will result in
// the tabs joining the same group as the last tab.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragMultipleTabsRightIntoGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  // Create a browser with four tabs total, and put each tab into its own
  // single-tab group.
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 3);

  tab_groups::TabGroupId group2 = model->AddToNewGroup({1});
  tab_groups::TabGroupId group4 = model->AddToNewGroup({3});

  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  // Click the first tab and select third tab so both first and third tabs are
  // selected.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  browser()->GetTabStripModel()->SelectTabAt(2);

  // Drag the first tab from its left edge toward the right. This should make
  // the two selected tabs join the start of Tab Group 2 (which previously held
  // only the second tab).
  ASSERT_TRUE(
      PressInput(test::GetLeftCenterInScreenCoordinates(tab_strip->tab_at(0)),
                 GetWindowHint(tab_strip)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("0 2 1 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group4)->ListTabs(), gfx::Range(3, 4));
  EXPECT_EQ(group_model->GetTabGroup(group2)->ListTabs(), gfx::Range(0, 3));

  // Dragging the center of the first tab to the center of the third tab will
  // result in the tabs joining the end of Tab Group 4.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("1 3 0 2", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group2)->ListTabs(), gfx::Range(0, 1));
  EXPECT_EQ(group_model->GetTabGroup(group4)->ListTabs(), gfx::Range(1, 4));
}

// Creates a browser with four tabs each. First and Third tabs are in their own
// group. Selecting and dragging the second and fourth tabs left at the fourth
// tab will result in the tabs joining the same group as the tab in the third
// position.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragMultipleTabsLeftIntoGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group1 = model->AddToNewGroup({0});
  tab_groups::TabGroupId group3 = model->AddToNewGroup({2});

  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  // Click the second tab and select fourth tab so both second and fourth tabs
  // are selected.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(ReleaseInput());
  browser()->GetTabStripModel()->SelectTabAt(3);

  // Dragging the fourth tab slightly to the left will result in the two
  // selected tabs joining the end of Tab Group 3.
  const gfx::Point left_center_fourth_tab =
      test::GetLeftCenterInScreenCoordinates(tab_strip->tab_at(3));

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(3)));
  ASSERT_TRUE(DragInputTo(left_center_fourth_tab, GetWindowHint(tab_strip)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("0 2 1 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(0, 1));
  EXPECT_EQ(group_model->GetTabGroup(group3)->ListTabs(), gfx::Range(1, 4));
}

// Creates a browser with four tabs with third and fourth in a group.
// Selecting and  dragging the second and third tabs towards left out
// of the group will result in the tabs being ungrouped.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragUngroupedTabGroupedTabOutsideGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group1 = model->AddToNewGroup({2, 3});
  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  // Click the second tab and select third tab so both second and third tabs
  // are selected.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(ReleaseInput());
  browser()->GetTabStripModel()->SelectTabAt(2);

  // Dragging the third tab slightly to the left will result in the two
  // selected tabs leaving the group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("1 2 0 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(3, 4));
}

// Drag a split tab within a tabstrip and cancel the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertDragSplitTab) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 3);
  model->ActivateTabAt(0);
  split_tabs::SplitTabId split_id =
      model->AddToNewSplit({1}, split_tabs::SplitTabVisualData(),
                           split_tabs::SplitTabCreatedSource::kToolbarButton);
  StopAnimating(tab_strip);

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(3)));

  EXPECT_EQ("2 3 0 1", IDString(model));
  EXPECT_EQ(model->GetSplitData(split_id)->ListTabs(),
            std::vector<tabs::TabInterface*>(
                {model->GetTabAtIndex(2), model->GetTabAtIndex(3)}));

  ASSERT_TRUE(TabDragController::IsActive());

  // Pressing escape will revert the tabs to original state before the drag.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_ESCAPE, false,
                                              false, false, false));
  EXPECT_EQ("0 1 2 3", IDString(model));
  EXPECT_EQ(model->GetSplitData(split_id)->ListTabs(),
            std::vector<tabs::TabInterface*>(
                {model->GetTabAtIndex(0), model->GetTabAtIndex(1)}));
}

// Creates a browser with four tabs. The first two tabs are in Tab Group 1.
// Dragging the third tab over one to the left will result in the tab joining
// Tab Group 1. While this drag is still in session, pressing escape will revert
// group of the tab to before the drag session started.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertDragSingleTabIntoGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group1 = model->AddToNewGroup({0, 1});
  StopAnimating(tab_strip);

  // Dragging the tab in the second index to the tab in the first index switches
  // the tabs and adds the dragged tab to the group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));

  EXPECT_EQ("0 2 1 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(0, 3));

  ASSERT_TRUE(TabDragController::IsActive());

  // Pressing escape will revert the tabs to original state before the drag.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_ESCAPE, false,
                                              false, false, false));
  EXPECT_EQ("0 1 2 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(0, 2));
}

// Creates a browser with four tabs. The
// second tab is in Tab Group 1. Dragging the second tab over to the end of the
// tabstrip . While this drag is still in session, pressing escape will revert
// group of the tab, but will not recreate the group because the group was
// closed during dragging.

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertDragSingleTabGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group1 = model->AddToNewGroup({1});
  const tab_groups::TabGroupVisualData new_data(
      u"Foo", tab_groups::TabGroupColorId::kCyan);
  group_model->GetTabGroup(group1)->SetVisualData(new_data);
  StopAnimating(tab_strip);

  // Dragging the tab in the first index to the tab in the second index switches
  // the tabs and adds the dragged tab to the group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(3)));

  EXPECT_EQ("0 2 3 1", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(3, 4));

  ASSERT_TRUE(TabDragController::IsActive());

  // Reverting the drag (by pressing escape) will not rebuild the group because
  // saved group information is not saved in the tab drag controller, so the
  // group would be duplicated.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_ESCAPE, false,
                                              false, false, false));
  EXPECT_EQ("0 1 2 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(1, 2));
}

// Creates a browser with four tabs. The middle two belong in the same Tab
// Group. Dragging the group header will result in both the grouped tabs moving
// together.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragGroupHeaderDragsGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({1, 2});
  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  ASSERT_EQ(4, model->count());
  ASSERT_EQ(2u, group_model->GetTabGroup(group)->ListTabs().length());

  // Drag the entire group right by its header.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->group_header(group)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(3)));
  EXPECT_TRUE(group_model->GetTabGroup(group)->visual_data()->is_collapsed());
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("0 3 1 2", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group)->ListTabs(), gfx::Range(2, 4));
  EXPECT_FALSE(group_model->GetTabGroup(group)->visual_data()->is_collapsed());
  EXPECT_EQ(std::nullopt, model->GetTabGroupForTab(0));
  EXPECT_EQ(std::nullopt, model->GetTabGroupForTab(1));

  // Drag the entire group left by its header.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->group_header(group)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));
  EXPECT_TRUE(group_model->GetTabGroup(group)->visual_data()->is_collapsed());
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("1 2 0 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group)->ListTabs(), gfx::Range(0, 2));
  EXPECT_FALSE(group_model->GetTabGroup(group)->visual_data()->is_collapsed());
  EXPECT_EQ(std::nullopt, model->GetTabGroupForTab(2));
  EXPECT_EQ(std::nullopt, model->GetTabGroupForTab(3));
}

// Creates a browser with four tabs. The first two belong in Tab Group 1, and
// the last two belong in Tab Group 2. Dragging the group header of Tab Group 1
// right will result in Tab Group 1 moving but avoiding Tab Group 2.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragGroupHeaderRightAvoidsOtherGroups) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group1 = model->AddToNewGroup({0, 1});
  tab_groups::TabGroupId group2 = model->AddToNewGroup({2, 3});
  StopAnimating(tab_strip);

  ASSERT_EQ(4, model->count());
  ASSERT_EQ(2u, group_model->GetTabGroup(group1)->ListTabs().length());
  ASSERT_EQ(2u, group_model->GetTabGroup(group2)->ListTabs().length());

  // Drag group1 right, but not far enough to get to the other side of group2.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->group_header(group1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Expect group1 to "snap back" to its current position, avoiding group2.
  EXPECT_EQ("0 1 2 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(0, 2));
  EXPECT_EQ(group_model->GetTabGroup(group2)->ListTabs(), gfx::Range(2, 4));

  // Drag group1 right, far enough to get to the other side of group2.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->group_header(group1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Expect group1 to "snap to" the other side of group2 and not land in the
  // middle.
  EXPECT_EQ("2 3 0 1", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(2, 4));
  EXPECT_EQ(group_model->GetTabGroup(group2)->ListTabs(), gfx::Range(0, 2));
}

// Creates a browser with four tabs. The first two belong in Tab Group 1, and
// the last two belong in Tab Group 2. Dragging the group header of Tab Group 2
// left will result in Tab Group 2 moving but avoiding Tab Group 1.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragGroupHeaderLeftAvoidsOtherGroups) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group1 = model->AddToNewGroup({0, 1});
  tab_groups::TabGroupId group2 = model->AddToNewGroup({2, 3});
  StopAnimating(tab_strip);

  ASSERT_EQ(4, model->count());
  ASSERT_EQ(2u, group_model->GetTabGroup(group1)->ListTabs().length());
  ASSERT_EQ(2u, group_model->GetTabGroup(group2)->ListTabs().length());

  // Drag group2 left, but not far enough to get to the other side of group1.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->group_header(group2)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Expect group2 to "snap back" to its current position, avoiding group1.
  EXPECT_EQ("0 1 2 3", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(0, 2));
  EXPECT_EQ(group_model->GetTabGroup(group2)->ListTabs(), gfx::Range(2, 4));

  // Drag group2 left, far enough to get to the other side of group1.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->group_header(group2)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Expect group2 to "snap to" the other side of group1 and not land in the
  // middle.
  EXPECT_EQ("2 3 0 1", IDString(model));
  EXPECT_EQ(group_model->GetTabGroup(group1)->ListTabs(), gfx::Range(2, 4));
  EXPECT_EQ(group_model->GetTabGroup(group2)->ListTabs(), gfx::Range(0, 2));
}

// Creates a browser with four tabs. The first tab is pinned, and the last
// three belong in the same Tab Group. Dragging the pinned tab to the middle
// of the group will not result in the pinned tab getting grouped or moving
// into the group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragPinnedTabDoesNotGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();

  AddTabsAndResetBrowser(browser(), 3);
  model->SetTabPinned(0, true);
  tab_groups::TabGroupId group = model->AddToNewGroup({1, 2, 3});
  StopAnimating(tab_strip);

  ASSERT_EQ(4, model->count());
  ASSERT_EQ(3u, model->group_model()->GetTabGroup(group)->ListTabs().length());

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // The pinned tab should not have moved or joined the group.
  EXPECT_EQ("0 1 2 3", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(1, 4));
  EXPECT_EQ(std::nullopt, model->GetTabGroupForTab(0));
}

// Drags a tab within the window (without dragging the whole window) then
// pressing a key ends the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       KeyPressShouldEndDragTest) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));

  ASSERT_TRUE(TabDragController::IsActive());

  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_TAB, false,
                                              false, false, false));
  StopAnimating(tab_strip);

  EXPECT_EQ("1 0", IDString(browser()->GetTabStripModel()));
  EXPECT_FALSE(TabDragController::IsActive());
  EXPECT_FALSE(IsDragSessionActive(tab_strip));
}

// Flaky. https://crbug.com/343188577
#if BUILDFLAG(IS_CHROMEOS)
#define MAYBE_StartDragWhileEndingPreviousDragDoesNothingTest \
  DISABLED_StartDragWhileEndingPreviousDragDoesNothingTest
#else
#define MAYBE_StartDragWhileEndingPreviousDragDoesNothingTest \
  StartDragWhileEndingPreviousDragDoesNothingTest
#endif

// Can't start another drag session while the previous one is still ending.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_StartDragWhileEndingPreviousDragDoesNothingTest) {
  AddTabsAndResetBrowser(browser(), 2);

  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  const gfx::Point tab_0_center_screen =
      GetCenterInScreenCoordinates(tab_strip->tab_at(0));
  const gfx::Point tab_2_center_screen =
      GetCenterInScreenCoordinates(tab_strip->tab_at(2));

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));

  ASSERT_TRUE(TabDragController::IsActive());
  StopAnimating(tab_strip);

  ASSERT_TRUE(ReleaseInput());

  // The drag is over...
  ASSERT_FALSE(TabDragController::IsActive());
  ASSERT_FALSE(IsDragSessionActive(tab_strip));

  // But the tab is still animating and still belongs to the TabDragContext.
  ASSERT_TRUE(tab_strip->IsAnimatingInTabStrip());
  ASSERT_TRUE(tab_strip->tab_at(0)->dragging());
  ASSERT_EQ(tab_strip->tab_at(0)->parent(), tab_strip->GetDragContext());

  ASSERT_EQ("1 0 2", IDString(browser()->GetTabStripModel()));

  // Attempt to start *another* drag session while the animation is still going.
  ASSERT_TRUE(PressInput(tab_2_center_screen, GetWindowHint(tab_strip)));
  ASSERT_TRUE(DragInputTo(tab_0_center_screen, GetWindowHint(tab_strip)));

  // This should not actually start.
  EXPECT_FALSE(TabDragController::IsActive());
  EXPECT_FALSE(IsDragSessionActive(tab_strip));
  EXPECT_EQ("1 0 2", IDString(browser()->GetTabStripModel()));

  ASSERT_TRUE(ReleaseInput());
}

#if defined(USE_AURA)
bool SubtreeShouldBeExplored(aura::Window* window,
                             const gfx::Point& local_point) {
  gfx::Point point_in_parent = local_point;
  aura::Window::ConvertPointToTarget(window, window->parent(),
                                     &point_in_parent);
  gfx::Point point_in_root = local_point;
  aura::Window::ConvertPointToTarget(window, window->GetRootWindow(),
                                     &point_in_root);
  ui::MouseEvent event(ui::EventType::kMouseMoved, point_in_parent,
                       point_in_root, base::TimeTicks::Now(), 0, 0);
  return window->targeter()->SubtreeShouldBeExploredForEvent(window, event);
}

// The logic to find the target tabstrip should take the window mask into
// account. This test hangs without the fix. crbug.com/40412236.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragWithMaskedWindows) {
  AddTabsAndResetBrowser(browser(), 1);

  aura::Window* browser_window = browser()->GetWindow()->GetNativeWindow();
  const gfx::Rect bounds = browser_window->GetBoundsInScreen();
  aura::test::TestWindowDelegate masked_window_delegate;
  masked_window_delegate.set_can_focus(false);
  std::unique_ptr<aura::Window> masked_window =
      aura::test::CreateTestWindow({.delegate = &masked_window_delegate,
                                    .parent = browser_window->parent(),
                                    .bounds = bounds,
                                    .window_id = 10});
  masked_window->SetProperty(aura::client::kZOrderingKey,
                             ui::ZOrderLevel::kFloatingWindow);
  auto targeter = std::make_unique<aura::WindowTargeter>();
  targeter->SetInsets(gfx::Insets::TLBR(0, bounds.width() - 10, 0, 0));
  masked_window->SetEventTargeter(std::move(targeter));

  ASSERT_FALSE(SubtreeShouldBeExplored(masked_window.get(),
                                       gfx::Point(bounds.width() - 11, 0)));
  ASSERT_TRUE(SubtreeShouldBeExplored(masked_window.get(),
                                      gfx::Point(bounds.width() - 9, 0)));
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("1 0", IDString(model));
  EXPECT_FALSE(TabDragController::IsActive());
  EXPECT_FALSE(IsDragSessionActive(tab_strip));
}
#endif  // USE_AURA

namespace {

// Invoked from the nested run loop.
void DragToSeparateWindowStep2(DetachToBrowserTabDragControllerTest* test,
                               BrowserWindowInterface* not_attached_browser,
                               BrowserWindowInterface* target_browser) {
  TabStrip* const not_attached_tab_strip =
      GetTabStripForBrowser(not_attached_browser);
  TabStrip* const target_tab_strip = GetTabStripForBrowser(target_browser);
  EXPECT_FALSE(IsDragSessionActive(not_attached_tab_strip));
  EXPECT_FALSE(IsDragSessionActive(target_tab_strip));
  EXPECT_TRUE(TabDragController::IsActive());

  // Test that after the tabs are detached from the source tabstrip (in this
  // case `not_attached_tab_strip`), the tab dragging info should be properly
  // cleared on the source tabstrip.
  EXPECT_TRUE(test->IsTabDraggingInfoCleared(not_attached_tab_strip));
  // At this moment there should be a new browser window for the dragged tabs.
  ASSERT_EQ(3u, GlobalBrowserCollection::GetInstance()->GetSize());
  BrowserWindowInterface* const new_browser =
      ui_test_utils::GetBrowserNotInSet({not_attached_browser, target_browser});
  ASSERT_TRUE(new_browser);

  TabStrip* const new_tab_strip = GetTabStripForBrowser(new_browser);
  EXPECT_TRUE(IsDragSessionActive(new_tab_strip));
  // Test that the tab dragging info should be correctly set on the new window.
  EXPECT_TRUE(test->IsTabDraggingInfoSet(new_tab_strip));

  // Drag to target_tab_strip. This should stop the nested loop from dragging
  // the window.
  //
  // Note: It's possible on small screens for the windows to overlap, so we want
  // to pick a point squarely within the second tab strip and not in the
  // starting window. This only works if the platform supports global screen
  // coordinates; on platforms without this support the test environment must
  // ensure a large enough screen (e.g. xvfb.py for Linux) to make sure that the
  // windows don't overlap.
  if (test::PlatformSupportsScreenCoordinates()) {
    const gfx::Rect old_window_bounds =
        not_attached_tab_strip->GetWidget()->GetWindowBoundsInScreen();
    const gfx::Rect target_bounds = target_tab_strip->GetBoundsInScreen();
    const gfx::Point target_point = test::DragPointInTabStrip(
        target_bounds, old_window_bounds, target_bounds.right() - 50);
    EXPECT_TRUE(target_bounds.Contains(target_point));
    EXPECT_TRUE(test->DragInputToAsync(target_point,
                                       test->GetWindowHint(target_tab_strip)));
  } else {
    const int offset_x = target_tab_strip->width() / 2 - 50;
    EXPECT_TRUE(test->DragInputToCenterAsync(target_tab_strip,
                                             gfx::Vector2d(offset_x, 0)));
  }
}

}  // namespace

// Flaky. https://crbug.com/40748225
#if BUILDFLAG(IS_MAC) && defined(ARCH_CPU_ARM64)
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/40734863
#define MAYBE_DragToSeparateWindow DISABLED_DragToSeparateWindow
#else
#define MAYBE_DragToSeparateWindow DragToSeparateWindow
#endif

// Creates two browsers, drags from first into second.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragToSeparateWindow) {

  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);
  ui_test_utils::WaitForBrowserSetLastActive(browser());

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  ui_test_utils::WaitForBrowserSetLastActive(browser2);
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);

  Tab* tab = tab_strip->tab_at(0);
  ASSERT_TRUE(PressInputAtCenter(tab));
  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      tab,
      base::BindOnce(&DragToSeparateWindowStep2, this, browser(), browser2),
      gfx::Vector2d(0, GetDetachY(tab_strip))));

  // Wait for the browser containing the dragged tabs to be removed when
  // attaching to browser2.
  test::BrowserChangeWaiter(test::BrowserChangeWaiter::ChangeType::kRemoved)
      .Wait(base::BindLambdaForTesting([=, this]() {
        // Should now be attached to tab_strip2.
        ASSERT_TRUE(IsDragSessionActive(tab_strip2));
        ASSERT_FALSE(IsDragSessionActive(tab_strip));
        ASSERT_TRUE(TabDragController::IsActive());
        EXPECT_FALSE(GetIsDragged(browser()));
        EXPECT_TRUE(IsTabDraggingInfoCleared(tab_strip));
        EXPECT_TRUE(IsTabDraggingInfoSet(tab_strip2));

        // Drag to the trailing end of the tabstrip to ensure we're in a
        // predictable spot within the strip.
        StopAnimating(tab_strip2);
        ASSERT_TRUE(DragInputToCenter(tab_strip2->tab_at(1)));

        // Release mouse or touch, stopping the drag session.
        ASSERT_TRUE(ReleaseInput());
      }));

  ASSERT_FALSE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_TRUE(IsTabDraggingInfoCleared(tab_strip2));
  EXPECT_EQ("100 0", IDString(browser2->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));
  EXPECT_FALSE(GetIsDragged(browser2));

  // Both windows should not be maximized
  EXPECT_FALSE(browser()->GetWindow()->IsMaximized());
  EXPECT_FALSE(browser2->GetWindow()->IsMaximized());

  // The tab strip should no longer have capture because the drag was ended and
  // mouse/touch was released.
  EXPECT_FALSE(tab_strip->GetWidget()->HasCapture());
  EXPECT_FALSE(tab_strip2->GetWidget()->HasCapture());
}

// Test is based on DragToSeparateWindow. https://crbug.com/40748225
#if BUILDFLAG(IS_MAC) || BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX)
#define MAYBE_DragToSeparateWindowDuringDragEnd \
  DISABLED_DragToSeparateWindowDuringDragEnd
#else
#define MAYBE_DragToSeparateWindowDuringDragEnd \
  DragToSeparateWindowDuringDragEnd
#endif

// Creates two browsers, starts and end a drag in the target browser, then drags
// from source to target before the target browser finishes ending drags.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragToSeparateWindowDuringDragEnd) {

  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  AddTabsAndResetBrowser(browser2, 1);
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);

  ASSERT_TRUE(PressInputAtCenter(tab_strip2->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip2->tab_at(1)));

  StopAnimating(tab_strip2);
  ASSERT_TRUE(ReleaseInput());

  // We should be doing the post-drag animation.
  ASSERT_TRUE(base::test::RunUntil([&]() {
    return tab_strip2->GetDragContext()
               ->GetPositioningDelegate()
               ->IsAnimatingDragEnd() &&
           tab_strip2->tab_at(1)->dragging();
  }));

  Tab* tab = tab_strip->tab_at(0);
  ASSERT_TRUE(PressInputAtCenter(tab));
  // Drag from `tab_strip` to `tab_strip2`.
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      tab,
      base::BindOnce(&DragToSeparateWindowStep2, this, browser(), browser2),
      gfx::Vector2d(0, GetDetachY(tab_strip))));

  test::BrowserChangeWaiter(test::BrowserChangeWaiter::ChangeType::kRemoved)
      .Wait(base::BindLambdaForTesting([=, this]() {
        // Should now be attached to `tab_strip2`.
        ASSERT_TRUE(IsDragSessionActive(tab_strip2));
        ASSERT_FALSE(IsDragSessionActive(tab_strip));
        ASSERT_TRUE(TabDragController::IsActive());
        EXPECT_FALSE(GetIsDragged(browser()));
        EXPECT_TRUE(IsTabDraggingInfoCleared(tab_strip));
        EXPECT_TRUE(IsTabDraggingInfoSet(tab_strip2));

        // Release mouse or touch, stopping the drag session.
        ASSERT_TRUE(ReleaseInput());
      }));

  ASSERT_FALSE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
}


// Canceling tab dragging while the detached tab is reattached should
// not cause dangling ptr. (crbug.com/459242414)
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachAndAttachThenCancel) {
#if BUILDFLAG(IS_OZONE)
  if (::ui::OzonePlatform::RunningOnWaylandForTest()) {
    GTEST_SKIP() << "Wayland-chrome doesn't cancel tab dragging on esc.";
  }
#endif

  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);

  Tab* tab = tab_strip->tab_at(0);
  ASSERT_TRUE(PressInputAtCenter(tab));
  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      tab,
      base::BindOnce(&DragToSeparateWindowStep2, this, browser(), browser2),
      gfx::Vector2d(0, GetDetachY(tab_strip))));

  // Wait for the browser containing the dragged tabs to be removed when
  // attaching to browser2.
  test::BrowserChangeWaiter(test::BrowserChangeWaiter::ChangeType::kRemoved)
      .Wait(base::BindLambdaForTesting([=, this]() {
        StopAnimating(tab_strip2);
        ASSERT_TRUE(DragInputToCenter(tab_strip2->tab_at(1)));

        // Should now be attached to tab_strip2.
        EXPECT_EQ("100 0", IDString(browser2->GetTabStripModel()));
        EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));
        ASSERT_TRUE(TabDragController::IsActive());

        // Press escape to cancel drag.
        EXPECT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
            browser()->GetWindow()->GetNativeWindow(), ui::VKEY_ESCAPE, false,
            false, false, false));
      }));

  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("100", IDString(browser2->GetTabStripModel()));
  EXPECT_EQ("0 1", IDString(browser()->GetTabStripModel()));
}

#if BUILDFLAG(IS_WIN)

// Create two browsers, with the second one occluded, and drag from first over
// second. This should create a third browser, w/o bringing forward the second
// browser, because it's occluded.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragToOccludedWindow) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);

  // Mark the second browser as occluded. NativeWindow occlusion calculation has
  // been disabled in test constructor, so we don't need an actual occluding
  // window.
  browser2->GetWindow()
      ->GetNativeWindow()
      ->GetHost()
      ->SetNativeWindowOcclusionState(aura::Window::OcclusionState::OCCLUDED,
                                      {});

  // Drag a tab from first browser to middle of first tab of the second,
  // occluded browser, and drop. This should create a third browser window.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(DragInputToCenterAsync(tab_strip2->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());

  EXPECT_EQ(3u, GlobalBrowserCollection::GetInstance()->GetSize());
}

#endif  // BUILDFLAG(IS_WIN)

namespace {

// WindowFinder that calls OnMouseCaptureLost() from
// GetLocalProcessWindowAtPoint().
class CaptureLoseWindowFinder : public WindowFinder {
 public:
  explicit CaptureLoseWindowFinder(TabStrip* tab_strip)
      : tab_strip_(tab_strip) {}
  CaptureLoseWindowFinder(const CaptureLoseWindowFinder&) = delete;
  CaptureLoseWindowFinder& operator=(const CaptureLoseWindowFinder&) = delete;
  ~CaptureLoseWindowFinder() override = default;

  // WindowFinder:
  gfx::NativeWindow GetLocalProcessWindowAtPoint(
      const gfx::Point& screen_point,
      const std::set<gfx::NativeWindow>& ignore) override {
    static_cast<views::View*>(tab_strip_->GetDragContext())
        ->OnMouseCaptureLost();
    return gfx::NativeWindow();
  }

 private:
  raw_ptr<TabStrip> tab_strip_;
};

}  // namespace

// Calls OnMouseCaptureLost() from WindowFinder::GetLocalProcessWindowAtPoint()
// and verifies we don't crash. This simulates a crash seen on windows.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       CaptureLostDuringDrag) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);

  // Press on first tab so drag is active. Reset WindowFinder to one that causes
  // capture to be lost from within GetLocalProcessWindowAtPoint(), then
  // continue drag. The capture lost should trigger the drag to cancel.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(IsDragSessionActive(tab_strip));
  SetWindowFinderForTabStrip(
      tab_strip, base::WrapUnique(new CaptureLoseWindowFinder(tab_strip)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
}

namespace {

#if BUILDFLAG(IS_CHROMEOS)
bool IsWindowPositionManaged(aura::Window* window) {
  return window->GetProperty(ash::kWindowPositionManagedTypeKey);
}
bool HasUserChangedWindowPositionOrSize(aura::Window* window) {
  return ash::WindowState::Get(window)->bounds_changed_by_user();
}
#else
bool IsWindowPositionManaged(gfx::NativeWindow window) {
  return true;
}
bool HasUserChangedWindowPositionOrSize(gfx::NativeWindow window) {
  return false;
}
#endif

}  // namespace

// Drags from browser to separate window and releases mouse.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachToOwnWindow) {
  const gfx::Rect initial_bounds(browser()->GetWindow()->GetBounds());
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  tabs::TabHandle dragged_tab =
      browser()->GetTabStripModel()->GetTabAtIndex(0)->GetHandle();

  // Move to the first tab and drag it enough so that it detaches.
  int tab_0_width = tab_strip->tab_at(0)->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(), base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                    ReleaseInputAfterWindowDetached,
                                base::Unretained(this), tab_0_width));
  ASSERT_TRUE(new_browser);

  // Should no longer be dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  WaitForBrowserActivation(new_browser);
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  EXPECT_FALSE(IsDragSessionActive(tab_strip2));

  EXPECT_EQ("0", IDString(new_browser->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));
  EXPECT_EQ(0,
            new_browser->GetTabStripModel()->GetIndexOfTab(dragged_tab.Get()));
  EXPECT_EQ(new_browser->GetTabStripModel(),
            dragged_tab.Get()->GetBrowserWindowInterface()->GetTabStripModel());

  // The bounds of the initial window should not have changed.
  EXPECT_EQ(initial_bounds.ToString(),
            browser()->GetWindow()->GetBounds().ToString());

  EXPECT_FALSE(GetIsDragged(browser()));
  EXPECT_FALSE(GetIsDragged(new_browser));
  // After this both windows should still be manageable.
  EXPECT_TRUE(
      IsWindowPositionManaged(browser()->GetWindow()->GetNativeWindow()));
  EXPECT_TRUE(
      IsWindowPositionManaged(new_browser->GetWindow()->GetNativeWindow()));

  // Both windows should not be maximized
  EXPECT_FALSE(browser()->GetWindow()->IsMaximized());
  EXPECT_FALSE(new_browser->GetWindow()->IsMaximized());

  // The tab strip should no longer have capture because the drag was ended and
  // mouse/touch was released.
  EXPECT_FALSE(tab_strip->GetWidget()->HasCapture());
  EXPECT_FALSE(tab_strip2->GetWidget()->HasCapture());
}

class ActiveTabTracker : public TabStripModelObserver {
 public:
  explicit ActiveTabTracker(TabStripModel* model) : model_(model) {
    model_->AddObserver(this);
  }
  ActiveTabTracker(const ActiveTabTracker&) = delete;
  ActiveTabTracker& operator=(const ActiveTabTracker&) = delete;
  ~ActiveTabTracker() override { model_->RemoveObserver(this); }

  void OnTabStripModelChanged(
      TabStripModel* tab_strip_model,
      const TabStripModelChange& change,
      const TabStripSelectionChange& selection) override {
    if (selection.active_tab_changed() && selection.new_contents) {
      active_contents_.push_back(selection.new_contents);
    }
  }

  bool WasActivated(content::WebContents* contents) const {
    return std::ranges::find(active_contents_, contents) !=
           active_contents_.end();
  }

 private:
  std::vector<content::WebContents*> active_contents_;
  raw_ptr<TabStripModel> model_;
};

// Verifies that dragging a background tab out to a new window does not
// cause the adjacent background tab to briefly gain focus.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachBackgroundTab_NoFocusShift) {
  // Create 3 tabs total (1 default + 2 new).
  AddTabsAndResetBrowser(browser(), 2);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();

  // Activate Tab 0 explicitly
  model->ActivateTabAt(0);
  EXPECT_EQ(0, model->active_index());

  content::WebContents* tab2 = model->GetWebContentsAt(2);

  ActiveTabTracker tracker(model);

  int tab_1_width = tab_strip->tab_at(1)->width();

  // Move to Tab 1 and drag it enough so that it detaches.
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(),
      base::BindOnce(&DetachToBrowserTabDragControllerTest::
                         ReleaseInputAfterWindowDetached,
                     base::Unretained(this), tab_1_width),
      /*tab_index=*/1);
  ASSERT_TRUE(new_browser);

  // Should no longer be dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  WaitForBrowserActivation(new_browser);
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  EXPECT_FALSE(IsDragSessionActive(tab_strip2));

  // Verify the new window has the dragged tab and the original has the other
  // two.
  EXPECT_EQ("1", IDString(new_browser->GetTabStripModel()));
  EXPECT_EQ("0 2", IDString(browser()->GetTabStripModel()));

  // The crucial check: Tab 2 should never have been activated in the original
  // window.
  EXPECT_FALSE(tracker.WasActivated(tab2));
}

// Tests that dragging a tab from a window, attaching it to another window, and
// then detaching it again retains the original window's size.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachAttachDetachRetainsOriginalSize) {
  BrowserWindowInterface* browser_large = browser();
  // Set up two windows with different sizes. `browser_large` is the source.
  AddTabsAndResetBrowser(browser_large, 1);
  TabStrip* tab_strip_large = GetTabStripForBrowser(browser_large);
  if (!tab_strip_large->GetWidget()->IsMoveLoopSupported()) {
    GTEST_SKIP() << "Not relevant for fallback tab dragging";
  }
  tabs::TabHandle dragged_tab =
      browser_large->GetTabStripModel()->GetTabAtIndex(0)->GetHandle();

  BrowserWindowInterface* browser_small = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip_small = GetTabStripForBrowser(browser_small);
  ui_test_utils::WaitForBrowserSetLastActive(browser_small);

  constexpr float kSmallWindowScaleFactor = 0.5f;
  gfx::Rect small_rect(
      browser_small->GetWindow()->GetBounds().origin(),
      gfx::ScaleToFlooredSize(browser_small->GetWindow()->GetBounds().size(),
                              kSmallWindowScaleFactor));
  ui_test_utils::SetAndWaitForBounds(*browser_small, small_rect);

  browser_large->GetWindow()->Activate();
  ui_test_utils::WaitForBrowserSetLastActive(browser_large);

  Tab* tab = tab_strip_large->tab_at(0);
  // Move to the first tab and drag it enough so that it detaches and attaches
  // to `browser_small`.
  ASSERT_TRUE(PressInputAtCenter(tab));
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      tab,
      base::BindOnce(&DragToSeparateWindowStep2, this, browser_large,
                     browser_small),
      gfx::Vector2d(0, GetDetachY(tab_strip_large))));

  // Wait for the temporary window to be removed (as a result of attachment).
  test::BrowserChangeWaiter(test::BrowserChangeWaiter::ChangeType::kRemoved)
      .Wait(base::BindLambdaForTesting([tab_strip_small, tab_strip_large,
                                        browser_small, browser_large, this]() {
        // Should now be attached to `tab_strip_small`.
        ASSERT_TRUE(IsDragSessionActive(tab_strip_small));
        ASSERT_FALSE(IsDragSessionActive(tab_strip_large));
        ASSERT_TRUE(TabDragController::IsActive());
        EXPECT_FALSE(GetIsDragged(browser_large));
        EXPECT_TRUE(IsTabDraggingInfoCleared(tab_strip_large));
        EXPECT_TRUE(IsTabDraggingInfoSet(tab_strip_small));

        // Drag to the trailing end of the tab strip to ensure we're in a
        // predictable spot within the strip.
        StopAnimating(tab_strip_small);
        ASSERT_TRUE(DragInputToCenter(tab_strip_small->tab_at(1)));

        ASSERT_TRUE(IsDragSessionActive(tab_strip_small));
        ASSERT_FALSE(IsDragSessionActive(tab_strip_large));
        EXPECT_EQ("100 0", IDString(browser_small->GetTabStripModel()));
        EXPECT_EQ("1", IDString(browser_large->GetTabStripModel()));

        StopAnimating(tab_strip_small);
      }));

  // Now continue the drag session to detach again. We use
  // `ContinueDragTabForDetachAndNotify` because the drag is already active and
  // we don't want to press input again.
  BrowserWindowInterface* browser_detached = ContinueDragTabForDetachAndNotify(
      browser_small,
      base::BindLambdaForTesting([this](BrowserWindowInterface* source,
                                        BrowserWindowInterface* detached) {
        // Release input to end the continuous drag session.
        ASSERT_TRUE(ReleaseInput(0, true));
      }),
      1);  // Index 1 is the newly attached tab

  ASSERT_TRUE(browser_detached);

  // Verify the results after the drag finishes.
  EXPECT_FALSE(IsDragSessionActive(tab_strip_small));
  EXPECT_FALSE(IsDragSessionActive(GetTabStripForBrowser(browser_detached)));
  EXPECT_EQ(3u, GlobalBrowserCollection::GetInstance()->GetSize());

  EXPECT_EQ("100", IDString(browser_small->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser_large->GetTabStripModel()));
  EXPECT_EQ("0", IDString(browser_detached->GetTabStripModel()));

  // Verify dragged status is cleared everywhere.
  EXPECT_FALSE(GetIsDragged(browser_large));
  EXPECT_FALSE(GetIsDragged(browser_small));
  EXPECT_FALSE(GetIsDragged(browser_detached));

  // Verify the specific tab was moved to the new browser.
  EXPECT_EQ(0, browser_detached->GetTabStripModel()->GetIndexOfTab(
                   dragged_tab.Get()));

  // Verify that the final window has the original size captured from
  // `browser_large`.
  EXPECT_EQ(browser_large->GetWindow()->GetBounds().size(),
            browser_detached->GetWindow()->GetBounds().size());

  // Both windows should not be maximized
  EXPECT_FALSE(browser_large->GetWindow()->IsMaximized());
  EXPECT_FALSE(browser_small->GetWindow()->IsMaximized());

  // Verify window management state is valid.
  EXPECT_TRUE(IsWindowPositionManaged(
      browser_detached->GetWindow()->GetNativeWindow()));

  // The tab strip should no longer have capture because the drag was ended and
  // mouse/touch was released.
  EXPECT_FALSE(tab_strip_large->GetWidget()->HasCapture());
  EXPECT_FALSE(tab_strip_small->GetWidget()->HasCapture());
}

class TabDragControllerTestDialog : public views::DialogDelegateView {
 public:
  TabDragControllerTestDialog() {
    SetFocusBehavior(FocusBehavior::ALWAYS);
    SetModalType(ui::mojom::ModalType::kChild);
    // Dialogs that take focus must have a name and role to pass accessibility
    // checks.
    GetViewAccessibility().SetRole(ax::mojom::Role::kDialog);
    GetViewAccessibility().SetName("Test dialog",
                                   ax::mojom::NameFrom::kAttribute);
  }

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

  ~TabDragControllerTestDialog() override = default;

  views::View* GetInitiallyFocusedView() override { return this; }
};

// Drags from browser that has a web dialog to separate window.
// The dialog should follow the new browser window.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachToOwnWindowWithDialog) {
  const gfx::Rect initial_bounds(browser()->GetWindow()->GetBounds());
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Create a web modal dialog on the first tab.
  views::Widget* dialog = constrained_window::ShowWebModalDialogViews(
      new TabDragControllerTestDialog,
      browser()->GetTabStripModel()->GetWebContentsAt(0));

  // Capture the initial offset of the dialog relative to the browser before
  // dragging.
  gfx::Point dialog_initial_position = dialog->GetRestoredBounds().origin();
  gfx::Vector2d initial_offset =
      dialog_initial_position - initial_bounds.origin();

  // Move to the first tab and drag it enough so that it detaches.
  int tab_0_width = tab_strip->tab_at(0)->width();
  gfx::Point initial_drag_position =
      GetCenterInScreenCoordinates(tab_strip->tab_at(0));
  int detach_y = GetDetachY(tab_strip);
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(),
      base::BindLambdaForTesting([&](BrowserWindowInterface* source_browser,
                                     BrowserWindowInterface* detached_browser) {
        // Drags further.
        DragInputTo(initial_drag_position + gfx::Vector2d(0, 2 * detach_y),
                    gfx::NativeWindow()),
            ReleaseInputAfterWindowDetached(tab_0_width, source_browser,
                                            detached_browser);
      }));
  ASSERT_TRUE(new_browser);

  // Check that the dialog's parent window is the new browser's window.
  EXPECT_EQ(dialog->parent(), views::Widget::GetWidgetForNativeWindow(
                                  new_browser->GetWindow()->GetNativeWindow()));

  // The relative offset from the new browser to the dialog should remain
  // unchanged after dragging.
  gfx::Point dialog_position = dialog->GetRestoredBounds().origin();
  gfx::Point browser_position = new_browser->GetWindow()->GetBounds().origin();
  gfx::Vector2d offset = dialog_position - browser_position;

  EXPECT_EQ(initial_offset, offset);

  // The bounds of the initial window should not have changed.
  EXPECT_EQ(initial_bounds.ToString(),
            browser()->GetWindow()->GetBounds().ToString());
}

// TODO(crbug.com/40934892): ChromeOS and Wayland flakes for tests that involve
// detaching to a new window.
#if !BUILDFLAG(IS_CHROMEOS)
class TabDragTargetTest : public DetachToBrowserTabDragControllerTest {
 public:
  TabDragTargetTest() {
    delegate_ = std::make_unique<test::FakeTabDragTarget>();
    resolver_ =
        std::make_unique<test::FakeTabDragPointResolver>(delegate_.get());
    SetTabDragPointResolver(*resolver_);
  }

  ~TabDragTargetTest() override = default;

  void SetUpOnMainThread() override {
#if BUILDFLAG(IS_OZONE)
    if (::ui::OzonePlatform::RunningOnWaylandForTest()) {
      GTEST_SKIP() << "Wayland has drag and drop limitations";
    }
#endif
    DetachToBrowserTabDragControllerTest::SetUpOnMainThread();
  }

 protected:
  std::unique_ptr<test::FakeTabDragTarget> delegate_;
  std::unique_ptr<test::FakeTabDragPointResolver> resolver_;
};

#if BUILDFLAG(IS_CHROMEOS)
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    TabDragTargetTest,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse", "touch")));
#else
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    TabDragTargetTest,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse")));
#endif  // BUILDFLAG(IS_CHROMEOS)

// Simple test that confirms the drag delegate receives the right events,
// and accepts the tab drop without doing anything.
IN_PROC_BROWSER_TEST_P(TabDragTargetTest, DragToDelegateAcceptsDrop) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  Tab* tab = tab_strip->tab_at(0);
  const int tab_width = tab->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(),
      base::BindLambdaForTesting([&](BrowserWindowInterface* source_browser,
                                     BrowserWindowInterface* detached_browser) {
        EXPECT_TRUE(delegate_->drag_entered());
        EXPECT_TRUE(delegate_->drag_updated());
        EXPECT_FALSE(delegate_->drag_exited());
        ReleaseInputAfterWindowDetached(tab_width, source_browser,
                                        detached_browser);
      }));
  ASSERT_TRUE(new_browser);

  // The drop should have been handled by the delegate.
  EXPECT_TRUE(delegate_->dropped());
  EXPECT_TRUE(delegate_->drag_ended());

  // The tab should not have moved.
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));
  EXPECT_EQ("0", IDString(new_browser->GetTabStripModel()));
}

// Simple test that confirms the drag delegate receives the right events,
// and rejects the drop.
IN_PROC_BROWSER_TEST_P(TabDragTargetTest, DelegateDeniesDrop) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  delegate_->set_can_drop(false);

  Tab* tab = tab_strip->tab_at(0);
  const int tab_width = tab->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(),
      base::BindLambdaForTesting([&](BrowserWindowInterface* source_browser,
                                     BrowserWindowInterface* detached_browser) {
        EXPECT_TRUE(delegate_->drag_entered());
        EXPECT_TRUE(delegate_->drag_updated());
        EXPECT_FALSE(delegate_->drag_exited());
        ReleaseInputAfterWindowDetached(tab_width, source_browser,
                                        detached_browser);
      }));
  ASSERT_TRUE(new_browser);

  // The drop should not have been handled by the delegate.
  EXPECT_FALSE(delegate_->dropped());
  EXPECT_TRUE(delegate_->drag_ended());

  // The tab should not have moved.
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));
  EXPECT_EQ("0", IDString(new_browser->GetTabStripModel()));
}

// Validates behavior when the drag delegate moves the dragged tab back to the
// source tab strip.
IN_PROC_BROWSER_TEST_P(TabDragTargetTest, DelegateMovesTabToSourceTabStrip) {
  AddTabsAndResetBrowser(browser(), 2);
  ASSERT_EQ("0 1 2", IDString(browser()->GetTabStripModel()));
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // The delegate inserts the dragged tab to index 1 of the source
  // tabstrip.
  delegate_->set_drop_callback(base::BindRepeating(
      [](BrowserWindowInterface* browser,
         TabDragTarget::DragController& controller) {
        auto tab = controller.DetachTabAtForInsertion(0);
        browser->GetTabStripModel()->InsertDetachedTabAt(1, std::move(tab), 0,
                                                         std::nullopt);
      },
      browser()));

  Tab* tab = tab_strip->tab_at(0);
  const int tab_width = tab->width();
  DragTabForDetachAndNotify(
      browser(), base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                    ReleaseInputAfterWindowDetached,
                                base::Unretained(this), tab_width));
  // The detached window should be closed, and the order of the source tab
  // strip has changed.
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ("1 0 2", IDString(browser()->GetTabStripModel()));
}

// Validates behavior when the drag delegate moves the dragged tab to another
// browser's window.
IN_PROC_BROWSER_TEST_P(TabDragTargetTest,
                       DelegateMovesTabToOtherBrowserTabStrip) {
  AddTabsAndResetBrowser(browser(), 1);
  ASSERT_EQ("0 1", IDString(browser()->GetTabStripModel()));
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  ASSERT_EQ("100", IDString(browser2->GetTabStripModel()));
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // The delegate inserts the dragged tab to index 0 of the new
  // tabstrip.
  delegate_->set_drop_callback(base::BindRepeating(
      [](BrowserWindowInterface* browser2,
         TabDragTarget::DragController& controller) {
        auto tab = controller.DetachTabAtForInsertion(0);
        browser2->GetTabStripModel()->InsertDetachedTabAt(0, std::move(tab), 0,
                                                          std::nullopt);
      },
      browser2));

  Tab* tab = tab_strip->tab_at(0);
  const int tab_width = tab->width();
  DragTabForDetachAndNotify(
      browser(), base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                    ReleaseInputAfterWindowDetached,
                                base::Unretained(this), tab_width));

  // The detached window should be closed, and the other browser contains the
  // dragged tab.
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));
  EXPECT_EQ("0 100", IDString(browser2->GetTabStripModel()));
}

// Validates behavior when the drag delegate moves a subset of multiple dragged
// tabs back to the source tab strip.
IN_PROC_BROWSER_TEST_P(TabDragTargetTest,
                       DelegateMovesSubsetOfTabsToSourceTabStrip) {
  AddTabsAndResetBrowser(browser(), 4);
  ASSERT_EQ("0 1 2 3 4", IDString(browser()->GetTabStripModel()));

  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Selects tabs 0,2,4 to be dragged.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  TabStripModel* model = browser()->GetTabStripModel();
  model->SelectTabAt(2);
  model->SelectTabAt(4);

  // The delegate moves tab 2 to the first index of the source tab strip,
  // and tab 0 to the 2nd position of the source tab strip.
  delegate_->set_drop_callback(base::BindRepeating(
      [](BrowserWindowInterface* browser,
         TabDragTarget::DragController& controller) {
        auto tab = controller.DetachTabAtForInsertion(1);
        browser->GetTabStripModel()->InsertDetachedTabAt(0, std::move(tab), 0,
                                                         std::nullopt);
        tab = controller.DetachTabAtForInsertion(0);
        browser->GetTabStripModel()->InsertDetachedTabAt(1, std::move(tab), 0,
                                                         std::nullopt);
      },
      browser()));

  Tab* tab = tab_strip->tab_at(0);
  const int tab_width = tab->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(), base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                    ReleaseInputAfterWindowDetached,
                                base::Unretained(this), tab_width));

  // Some of the dragged tabs are in the source tab strip, while others
  // remain in the new tab strip.
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ("2 0 1 3", IDString(browser()->GetTabStripModel()));
  EXPECT_EQ("4", IDString(new_browser->GetTabStripModel()));
}

// Validates behavior when the drag delegate rearranges multiple dragged tabs.
IN_PROC_BROWSER_TEST_P(TabDragTargetTest, DelegateRearrangesDraggedTabs) {
  AddTabsAndResetBrowser(browser(), 4);
  ASSERT_EQ("0 1 2 3 4", IDString(browser()->GetTabStripModel()));
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Selects tabs 0,2,4 to be dragged.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  TabStripModel* model = browser()->GetTabStripModel();
  model->SelectTabAt(2);
  model->SelectTabAt(4);

  // Moves tab 2 to the front of the detached window.
  delegate_->set_drop_callback(base::BindLambdaForTesting(
      [&](TabDragTarget::DragController& controller) {
        auto tab = controller.DetachTabAtForInsertion(1);
        CHECK_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
        BrowserWindowInterface* const new_browser =
            ui_test_utils::GetBrowserNotInSet({browser()});
        new_browser->GetTabStripModel()->InsertDetachedTabAt(0, std::move(tab),
                                                             0, std::nullopt);
      }));

  Tab* tab = tab_strip->tab_at(0);
  const int tab_width = tab->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(), base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                    ReleaseInputAfterWindowDetached,
                                base::Unretained(this), tab_width));
  ASSERT_TRUE(new_browser);

  EXPECT_EQ("1 3", IDString(browser()->GetTabStripModel()));
  EXPECT_EQ("2 0 4", IDString(new_browser->GetTabStripModel()));
}

// Validates behavior when the drag delegate moves a dragged tab that belongs
// to a group.
IN_PROC_BROWSER_TEST_P(TabDragTargetTest, DelegateRemovesDraggedTabFromGroup) {
  AddTabsAndResetBrowser(browser(), 3);
  ASSERT_EQ("0 1 2 3", IDString(browser()->GetTabStripModel()));
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();

  // Selects tabs 0,1, which are both in the group to be dragged.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  model->SelectTabAt(1);

  delegate_->set_drop_callback(base::BindRepeating(
      [](BrowserWindowInterface* browser,
         TabDragTarget::DragController& controller) {
        auto tab = controller.DetachTabAtForInsertion(0);
        browser->GetTabStripModel()->InsertDetachedTabAt(0, std::move(tab), 0,
                                                         std::nullopt);
      },
      browser()));

  Tab* tab = tab_strip->tab_at(0);
  const int tab_width = tab->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(), base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                    ReleaseInputAfterWindowDetached,
                                base::Unretained(this), tab_width));
  ASSERT_TRUE(new_browser);

  EXPECT_EQ("0 2 3", IDString(browser()->GetTabStripModel()));
  EXPECT_EQ("1", IDString(new_browser->GetTabStripModel()));
}
#endif  // !BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS)
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachToOwnWindowWithNonVisibleOnAllWorkspaceState) {
  // Set the source browser to be visible on all workspace.
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  BrowserWindowInterface* const source_browser =
      GetLastActiveBrowserWindowInterfaceWithAnyProfile();
  auto* source_window = source_browser->GetWindow()->GetNativeWindow();
  source_window->SetProperty(
      aura::client::kWindowWorkspaceKey,
      aura::client::kWindowWorkspaceVisibleOnAllWorkspaces);

  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  int tab_0_width = tab_strip->tab_at(0)->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(), base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                    ReleaseInputAfterWindowDetached,
                                base::Unretained(this), tab_0_width));
  ASSERT_TRUE(new_browser);

  // Should no longer be dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  WaitForBrowserActivation(new_browser);
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  EXPECT_FALSE(IsDragSessionActive(tab_strip2));

  EXPECT_EQ("0", IDString(new_browser->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));

  EXPECT_FALSE(GetIsDragged(browser()));
  EXPECT_FALSE(GetIsDragged(new_browser));
  // After this both windows should still be manageable.
  EXPECT_TRUE(
      IsWindowPositionManaged(browser()->GetWindow()->GetNativeWindow()));
  EXPECT_TRUE(
      IsWindowPositionManaged(new_browser->GetWindow()->GetNativeWindow()));

  auto* new_window = new_browser->GetWindow()->GetNativeWindow();
  // The new window should not be visible on all workspace.
  ASSERT_FALSE(new_window->GetProperty(aura::client::kWindowWorkspaceKey) ==
               aura::client::kWindowWorkspaceVisibleOnAllWorkspaces);
}
#endif  // BUILDFLAG(IS_CHROMEOS)

// Tests that a tab can be dragged from a browser window that is resized to full
// screen.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachFromFullsizeWindow) {
  // Resize the browser window so that it is as big as the work area.
  gfx::Rect work_area =
      display::Screen::Get()
          ->GetDisplayNearestWindow(browser()->GetWindow()->GetNativeWindow())
          .work_area();
  ui_test_utils::SetAndWaitForBounds(*browser(), work_area);
  const gfx::Rect initial_bounds(browser()->GetWindow()->GetBounds());
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  int tab_0_width = tab_strip->tab_at(0)->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(), base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                    ReleaseInputAfterWindowDetached,
                                base::Unretained(this), tab_0_width));
  ASSERT_TRUE(new_browser);

  // Should no longer be dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  WaitForBrowserActivation(new_browser);
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));

  EXPECT_EQ("0", IDString(new_browser->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));

  // The bounds of the initial window should not have changed.
  EXPECT_EQ(initial_bounds.ToString(),
            browser()->GetWindow()->GetBounds().ToString());

  EXPECT_FALSE(GetIsDragged(browser()));
  EXPECT_FALSE(GetIsDragged(new_browser));
  // After this both windows should still be manageable.
  EXPECT_TRUE(
      IsWindowPositionManaged(browser()->GetWindow()->GetNativeWindow()));
  EXPECT_TRUE(
      IsWindowPositionManaged(new_browser->GetWindow()->GetNativeWindow()));

  // The tab strip should no longer have capture because the drag was ended and
  // mouse/touch was released.
  EXPECT_FALSE(tab_strip->GetWidget()->HasCapture());
  EXPECT_FALSE(tab_strip2->GetWidget()->HasCapture());
}

// This test doesn't make sense on Mac, since it has no concept of "maximized".
#if !BUILDFLAG(IS_MAC)

// Drags from browser to a separate window and releases mouse.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachToOwnWindowFromMaximizedWindow) {
  // InitialWebUI defers window visibility. If we Maximize() before the window
  // is visible, the OS may silently cache or drop the request, causing tests to
  // hang on Wayland. Wait for visibility.
  ASSERT_TRUE(base::test::RunUntil([&]() {
    return browser()->GetWindow()->GetNativeWindow()->IsVisible();
  }));

  // Maximize the initial browser window.
  {
    auto waiter = ui_test_utils::CreateAsyncWidgetRequestWaiter(*browser());
    browser()->GetWindow()->Maximize();
    ASSERT_TRUE(base::test::RunUntil(
        [&]() { return browser()->GetWindow()->IsMaximized(); }));
    waiter.Wait();
  }

  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  int tab_0_width = tab_strip->tab_at(0)->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(), base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                    ReleaseInputAfterWindowDetached,
                                base::Unretained(this), tab_0_width));
  ASSERT_TRUE(new_browser);

  // Should no longer be dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  WaitForBrowserActivation(new_browser);
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));

  EXPECT_EQ("0", IDString(new_browser->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));

  // The bounds of the initial window should not have changed.
  EXPECT_TRUE(browser()->GetWindow()->IsMaximized());

  EXPECT_FALSE(GetIsDragged(browser()));
  EXPECT_FALSE(GetIsDragged(new_browser));
  // After this both windows should still be manageable.
  EXPECT_TRUE(
      IsWindowPositionManaged(browser()->GetWindow()->GetNativeWindow()));
  EXPECT_TRUE(
      IsWindowPositionManaged(new_browser->GetWindow()->GetNativeWindow()));

  const bool kMaximizedStateRetainedOnTabDrag =
#if BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)
      false;
#else
      true;
#endif  // BUILDFLAG(IS_WIN) || BUILDFLAG(IS_LINUX)

  if (kMaximizedStateRetainedOnTabDrag) {
    ASSERT_TRUE(base::test::RunUntil(
        [&]() { return new_browser->GetWindow()->IsMaximized(); }));
  }
  EXPECT_EQ(new_browser->GetWindow()->IsMaximized(),
            kMaximizedStateRetainedOnTabDrag);
}
#endif  // !BUILDFLAG(IS_MAC)

#if BUILDFLAG(IS_CHROMEOS)

// This test makes sense only on Chrome OS where we have the immersive
// fullscreen mode. The detached tab to a new browser window should remain in
// immersive fullscreen mode.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DetachToOwnWindowWhileInImmersiveFullscreenMode) {
  // Toggle the immersive fullscreen mode for the initial browser.
  chrome::ToggleFullscreenMode(browser());
  ImmersiveModeController* controller =
      ImmersiveModeController::From(browser());
  ASSERT_TRUE(controller->IsEnabled());

  // Forcively reveal the tabstrip immediately.
  std::unique_ptr<ImmersiveRevealedLock> lock =
      controller->GetRevealedLock(ImmersiveModeController::ANIMATE_REVEAL_NO);

  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  int tab_0_width = tab_strip->tab_at(0)->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(), base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                    ReleaseInputAfterWindowDetached,
                                base::Unretained(this), tab_0_width));
  ASSERT_TRUE(new_browser);

  // Should no longer be dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  WaitForBrowserActivation(new_browser);
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));

  EXPECT_EQ("0", IDString(new_browser->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));

  // The bounds of the initial window should not have changed.
  EXPECT_TRUE(browser()->GetWindow()->IsFullscreen());
  ASSERT_TRUE(controller->IsEnabled());

  EXPECT_FALSE(GetIsDragged(browser()));
  EXPECT_FALSE(GetIsDragged(new_browser));
  // After this both windows should still be manageable.
  EXPECT_TRUE(
      IsWindowPositionManaged(browser()->GetWindow()->GetNativeWindow()));
  EXPECT_TRUE(
      IsWindowPositionManaged(new_browser->GetWindow()->GetNativeWindow()));

  // The new browser should be in immersive fullscreen mode.
  ASSERT_TRUE(ImmersiveModeController::From(new_browser)->IsEnabled());
  EXPECT_TRUE(new_browser->GetWindow()->IsFullscreen());
}

#endif  // BUILDFLAG(IS_CHROMEOS)

// Deletes a tab being dragged before the user moved enough to start a drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DeleteBeforeStartedDragging) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Click on the first tab, but don't move it.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));

  // Should be dragging.
  ASSERT_TRUE(IsDragSessionActive(tab_strip));
  ASSERT_TRUE(TabDragController::IsActive());

  // Delete the tab being dragged.
  browser()->GetTabStripModel()->DetachAndDeleteWebContentsAt(0);

  // Should have canceled dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));
  EXPECT_FALSE(GetIsDragged(browser()));
}

// Replaces a tab being dragged before the user moved enough to start a drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       ReplaceBeforeStartedDragging) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Click on the first tab, but don't move it.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));

  // A drag session should exist, but the drag should not have started.
  ASSERT_TRUE(IsDragSessionActive(tab_strip));
  ASSERT_TRUE(TabDragController::IsActive());
  ASSERT_FALSE(HasDragStarted(tab_strip));

  // Replace the tab being dragged.
  std::unique_ptr<content::WebContents> new_web_contents =
      content::WebContents::Create(
          content::WebContents::CreateParams(browser()->GetProfile()));
  browser()->GetTabStripModel()->DiscardWebContents(
      browser()->GetTabStripModel()->GetWebContentsAt(0),
      std::move(new_web_contents));

  // The drag session should still exist, and still not be started.
  ASSERT_TRUE(IsDragSessionActive(tab_strip));
  ASSERT_TRUE(TabDragController::IsActive());
  ASSERT_FALSE(HasDragStarted(tab_strip));

  // Move the mouse enough to start the drag.  It doesn't matter whether this
  // is enough to create a window or not.
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0), gfx::Vector2d(40, 0)));

  // Drag should now have started.
  ASSERT_TRUE(HasDragStarted(tab_strip));

  // The replaced webcontents should not have an id character.
  EXPECT_EQ("? 1", IDString(browser()->GetTabStripModel()));

  EXPECT_TRUE(ReleaseInput());
}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragDoesntStartFromClick) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Click on the first tab, but don't move it.
  EXPECT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));

  // A drag session should exist, but the drag should not have started.
  EXPECT_TRUE(IsDragSessionActive(tab_strip));
  EXPECT_TRUE(TabDragController::IsActive());
  EXPECT_FALSE(HasDragStarted(tab_strip));

  // Move the mouse enough to start the drag.  It doesn't matter whether this
  // is enough to create a window or not.
  EXPECT_TRUE(DragInputToCenter(tab_strip->tab_at(0), gfx::Vector2d(20, 0)));

  // Drag should now have started.
  EXPECT_TRUE(HasDragStarted(tab_strip));

  EXPECT_TRUE(ReleaseInput());
}

// Deletes a tab being dragged while still attached.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DeleteTabWhileAttached) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Click on the first tab and move it enough so that it starts dragging but is
  // still attached.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0), gfx::Vector2d(20, 0)));

  // Should be dragging.
  ASSERT_TRUE(IsDragSessionActive(tab_strip));
  ASSERT_TRUE(TabDragController::IsActive());

  // Delete the tab being dragged.
  browser()->GetTabStripModel()->DetachAndDeleteWebContentsAt(0);

  // Should have canceled dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));

  EXPECT_FALSE(GetIsDragged(browser()));
}

// Selects 1 tab out of 4, drags it out and closes the new browser window while
// dragging.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DeleteTabsWhileDetached) {
  AddTabsAndResetBrowser(browser(), 3);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  EXPECT_EQ("0 1 2 3", IDString(browser()->GetTabStripModel()));

  ui_test_utils::BrowserDestroyedObserver browser_destroyed_observer;
  DragTabForDetachAndNotify(
      browser(),
      base::BindLambdaForTesting([&](BrowserWindowInterface* source_browser,
                                     BrowserWindowInterface* detached_browser) {
        ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
        EXPECT_EQ("0 1 3", IDString(source_browser->GetTabStripModel()));
        EXPECT_EQ("2", IDString(detached_browser->GetTabStripModel()));
        chrome::CloseTab(detached_browser);
        // Ensure that the newly created tab strip is "closeable" just after
        // requesting to close it, even if we are still waiting for the nested
        // move loop to exit. Regression test for https://crbug.com/40059182.
        EXPECT_TRUE(
            GetTabStripForBrowser(detached_browser)->IsTabStripCloseable());
      }),
      2);
  // Ensure completion of asynchronous browser destruction.
  browser_destroyed_observer.Wait();

  // Should no longer be dragging.
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  // Dragged out tab (and its owning window) should get closed.
  EXPECT_EQ("0 1 3", IDString(browser()->GetTabStripModel()));

  // No longer dragging.
  EXPECT_FALSE(GetIsDragged(browser()));
}

// Detaches a tab and while detached presses escape to revert the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertDragWhileDetached) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  tabs::TabHandle dragged_tab =
      browser()->GetTabStripModel()->GetTabAtIndex(0)->GetHandle();

  ui_test_utils::BrowserDestroyedObserver removed_observer;
  // Move to the first tab and drag it enough so that it detaches.
  DragTabForDetachAndNotify(
      browser(),
      base::BindLambdaForTesting([&](BrowserWindowInterface* source_browser,
                                     BrowserWindowInterface* detached_browser) {
        EXPECT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
            detached_browser->GetWindow()->GetNativeWindow(), ui::VKEY_ESCAPE,
            false, false, false, false));
      }));
  // Ensure completion of asynchronous browser closure.
  removed_observer.Wait();

  // Should not be dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  // And there should only be one window.
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  EXPECT_EQ("0 1", IDString(browser()->GetTabStripModel()));
  EXPECT_NE(nullptr, dragged_tab.Get());
  EXPECT_EQ(0, browser()->GetTabStripModel()->GetIndexOfTab(dragged_tab.Get()));
  EXPECT_EQ(browser()->GetTabStripModel(),
            dragged_tab.Get()->GetBrowserWindowInterface()->GetTabStripModel());

  // Remaining browser window should not be maximized
  EXPECT_FALSE(browser()->GetWindow()->IsMaximized());

  // The tab strip should no longer have capture because the drag was ended and
  // mouse/touch was released.
  EXPECT_FALSE(tab_strip->GetWidget()->HasCapture());
}

// Creates a browser with two tabs, drags the second to the first.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest, DragInSameWindow) {
  AddTabsAndResetBrowser(browser(), 1);

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));
  // Test that the dragging info is correctly set on `tab_strip`.
  EXPECT_TRUE(IsTabDraggingInfoSet(tab_strip));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("1 0", IDString(model));
  EXPECT_FALSE(TabDragController::IsActive());
  EXPECT_FALSE(IsDragSessionActive(tab_strip));
  // Test that the dragging info is properly cleared after dragging.
  EXPECT_TRUE(IsTabDraggingInfoCleared(tab_strip));

  // The tab strip should no longer have capture because the drag was ended and
  // mouse/touch was released.
  EXPECT_FALSE(tab_strip->GetWidget()->HasCapture());
}

// Tests that the TabDragPinnedness UMA metric is correctly logged for same
// window drags.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragInSameWindowPinnedness) {
  // Case 1: Drag an unpinned tab.
  {
    AddTabsAndResetBrowser(browser(), 2);
    TabStrip* tab_strip = GetTabStripForBrowser(browser());
    StopAnimating(tab_strip);

    base::HistogramTester histogram_tester;
    ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(2)));
    // Drag enough to ensure it starts.
    ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));
    ASSERT_TRUE(ReleaseInput());
    StopAnimating(tab_strip);
    histogram_tester.ExpectUniqueSample(
        "TabStrip.HorizontalTabStrip.TabDragPinnedness", 0 /* kAllUnpinned */,
        1);
  }

  // Case 2: Drag a pinned tab.
  {
    BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
    AddTabsAndResetBrowser(browser2, 2);
    TabStripModel* model2 = browser2->GetTabStripModel();
    TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
    model2->SetTabPinned(0, true);
    model2->ActivateTabAt(0);
    StopAnimating(tab_strip2);

    base::HistogramTester histogram_tester;
    ASSERT_TRUE(PressInputAtCenter(tab_strip2->tab_at(0)));
    // Drag further to the right.
    ASSERT_TRUE(DragInputToCenter(tab_strip2->tab_at(2)));
    ASSERT_TRUE(ReleaseInput());
    StopAnimating(tab_strip2);
    histogram_tester.ExpectUniqueSample(
        "TabStrip.HorizontalTabStrip.TabDragPinnedness", 1 /* kAllPinned */, 1);
  }

  // Case 3: Drag a mix of pinned and unpinned tabs.
  {
    BrowserWindowInterface* browser3 = CreateBrowser(browser()->GetProfile());
    AddTabsAndResetBrowser(browser3, 2);
    TabStripModel* model3 = browser3->GetTabStripModel();
    TabStrip* tab_strip3 = GetTabStripForBrowser(browser3);
    model3->SetTabPinned(0, true);
    ui::ListSelectionModel selection;
    selection.AddIndexToSelection(0);
    selection.AddIndexToSelection(1);
    selection.set_active(0);
    model3->SetSelectionFromModel(selection);
    StopAnimating(tab_strip3);

    base::HistogramTester histogram_tester;
    ASSERT_TRUE(PressInputAtCenter(tab_strip3->tab_at(0)));
    ASSERT_TRUE(DragInputToCenter(tab_strip3->tab_at(2)));
    ASSERT_TRUE(ReleaseInput());
    StopAnimating(tab_strip3);
    histogram_tester.ExpectUniqueSample(
        "TabStrip.HorizontalTabStrip.TabDragPinnedness", 2 /* kMixed */, 1);
  }
}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       TabDragContextOwnsDraggedTabs) {
  AddTabsAndResetBrowser(browser(), 1);

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabDragContext* drag_context = tab_strip->GetDragContext();
  Tab* dragged_tab = tab_strip->tab_at(1);

  // Tabs are not in the TabDragContext when they aren't being dragged.
  ASSERT_FALSE(drag_context->Contains(dragged_tab));

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  EXPECT_FALSE(drag_context->Contains(dragged_tab));

  // TabDragContext gets them only after the drag truly begins.
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));
  EXPECT_TRUE(drag_context->Contains(dragged_tab));

  // TabDragContext keeps them after the drag ends...
  ASSERT_TRUE(ReleaseInput());
  EXPECT_TRUE(drag_context->Contains(dragged_tab));

  // Until they animate back into place.
  StopAnimating(tab_strip);
  EXPECT_FALSE(drag_context->Contains(dragged_tab));

  EXPECT_FALSE(TabDragController::IsActive());
  EXPECT_FALSE(IsDragSessionActive(tab_strip));
}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       TabDragContextOwnsClosingDraggedTabs) {
  AddTabsAndResetBrowser(browser(), 1);

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabDragContext* drag_context = tab_strip->GetDragContext();
  Tab* dragged_tab = tab_strip->tab_at(1);

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));

  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));

  ASSERT_TRUE(base::test::RunUntil(
      [&]() { return tab_strip->tab_at(0) == dragged_tab; }));

  // Delete the tab being dragged.
  browser()->GetTabStripModel()->DetachAndDeleteWebContentsAt(0);

  // Should have canceled dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  // TabDragContext still owns the tab while it's closing.
  ASSERT_TRUE(dragged_tab->closing());
  EXPECT_TRUE(drag_context->Contains(dragged_tab));

  // Completing the close animation destroys the tab.
  views::ViewTracker dragged_tab_tracker(dragged_tab);
  StopAnimating(tab_strip);
  EXPECT_EQ(dragged_tab_tracker.view(), nullptr);
}

namespace {

void DragAllStep2(DetachToBrowserTabDragControllerTest* test) {
  // Should only be one window.
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Windows hangs if you use a sync mouse event here.
  EXPECT_TRUE(test->ReleaseInput(0, true));
}

}  // namespace

// Selects multiple tabs and starts dragging the window.
// TODO(crbug.com/509555634): Test is flaky on windows. Reenable once deflaked.
#if BUILDFLAG(IS_WIN)
#define MAYBE_DragAll DISABLED_DragAll
#else
#define MAYBE_DragAll DragAll
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest, MAYBE_DragAll) {
#if BUILDFLAG(IS_MAC)
  // TODO(crbug.com/510801992): Re-enable on macOS 26 once test is deflaked
  if (base::mac::MacOSMajorVersion() == 26) {
    GTEST_SKIP() << "Disabled on macOS Tahoe.";
  }
#endif
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  browser()->GetTabStripModel()->SelectTabAt(0);
  const gfx::Rect initial_bounds = browser()->GetWindow()->GetBounds();

  // Move to the first tab and drag it enough so that it would normally
  // detach.
  const int x_offset = 20;
  const gfx::Point tab_0_center =
      GetCenterInScreenCoordinates(tab_strip->tab_at(0));
  DragTabAndNotify(tab_strip, base::BindLambdaForTesting([&]() {
                     // Trigger a second event since in ChromeOS, `SetBounds` is
                     // only called after movement are detected within the
                     // moveloop. The first drag call only starts the moveloop.
                     // A small touch offset is used so touch movement is
                     // triggered rather than skip.
                     const gfx::NativeWindow window_hint =
                         GetWindowHint(tab_strip);
                     ASSERT_TRUE(DragInputToNotifyWhenDone(
                         gfx::Point(tab_0_center.x() + x_offset,
                                    tab_0_center.y() + GetDetachY(tab_strip)),
                         base::BindOnce(&DragAllStep2, this), window_hint));
                   }));

  // Should not be dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  // And there should only be one window.
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  EXPECT_EQ("0 1", IDString(browser()->GetTabStripModel()));

  EXPECT_FALSE(GetIsDragged(browser()));

  // Remaining browser window should not be maximized
  EXPECT_FALSE(browser()->GetWindow()->IsMaximized());

  const gfx::Rect final_bounds = browser()->GetWindow()->GetBounds();

  // The following expectations might not hold on platforms where we can't
  // control the browser's bounds.
  if (test::PlatformSupportsScreenCoordinates()) {
    // Size unchanged, but it should have moved down.
    EXPECT_EQ(initial_bounds.size(), final_bounds.size());
    EXPECT_EQ(initial_bounds.origin().x() + x_offset,
              final_bounds.origin().x());
    EXPECT_EQ(initial_bounds.origin().y() + GetDetachY(tab_strip),
              final_bounds.origin().y());
  }
}

#if BUILDFLAG(IS_MAC)
// Regression test for https://crbug.com/496402864. Unlike horizontal tabs,
// vertical tab strips are never hosted in the overlay widget, so
// CanRestoreFullscreenWindowDuringDrag() returns true for VT. Dragging the
// only tab in VT fullscreen should exit fullscreen before entering the move
// loop, allowing the user to drag the restored window normally.
class VerticalTabsFullscreenDragTest
    : public DetachToBrowserTabDragControllerTest {
 public:
  VerticalTabsFullscreenDragTest() = default;

  void SetUpOnMainThread() override {
    DetachToBrowserTabDragControllerTest::SetUpOnMainThread();
    browser()->GetProfile()->GetPrefs()->SetBoolean(prefs::kVerticalTabsEnabled,
                                                    true);
  }
};

// Verify that dragging the single tab in VT fullscreen exits fullscreen.
// When CanRestoreFullscreenWindowDuringDrag() returns true (VT case),
// RestoreAttachedWindowForDrag() exits fullscreen before RunMoveLoop().
// We wait for the fullscreen exit animation to complete before releasing
// the mouse, so AppKit doesn't swallow the mouse-up event.
IN_PROC_BROWSER_TEST_P(VerticalTabsFullscreenDragTest,
                       DragSingleTabExitsFullscreen) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser());
  ASSERT_TRUE(browser_view->ShouldDrawVerticalTabStrip());

  // Enter fullscreen and wait for the transition to complete.
  ui_test_utils::ToggleFullscreenModeAndWait(browser());
  ASSERT_TRUE(browser()->GetWindow()->IsFullscreen());

  ASSERT_EQ(1, browser()->GetTabStripModel()->count());

  TabDragContext* drag_context =
      browser_view->tab_strip_view()->GetDragContext();
  content::WebContents* contents =
      browser()->GetTabStripModel()->GetWebContentsAt(0);
  TabSlotView* tab_view = drag_context->GetTabForContents(contents);
  ASSERT_TRUE(tab_view);

  // FullscreenWaiter uses kNestableTasksAllowed so it works inside
  // RunMoveLoop's nested NSApplication event loop.
  ui_test_utils::FullscreenWaiter fullscreen_waiter(
      browser(), ui_test_utils::FullscreenWaiter::kNoFullscreen);

  // Drag the only tab. Since VT allows fullscreen restore, this will:
  // 1. Call RestoreAttachedWindowForDrag(), which exits fullscreen.
  // 2. Enter RunMoveLoop(), the nested run loop.
  ASSERT_TRUE(PressInputAtCenter(tab_view));
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      tab_view, base::BindLambdaForTesting([&]() {
        // Inside RunMoveLoop. Wait for the fullscreen exit animation to
        // fully complete before releasing the mouse. Without this wait,
        // AppKit may swallow the mouse-up during the transition, causing
        // RunMoveLoop to never exit (flaky timeout).
        fullscreen_waiter.Wait();

        // Now release the mouse. This cleanly ends RunMoveLoop.
        ASSERT_TRUE(ReleaseInput(0, /*async=*/true));
      }),
      gfx::Vector2d(0, 20)));

  // Wait for the drag to fully end.
  ASSERT_TRUE(
      base::test::RunUntil([&]() { return !TabDragController::IsActive(); }));

  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_FALSE(browser()->GetWindow()->IsFullscreen());
  EXPECT_EQ(1, browser()->GetTabStripModel()->count());
}

INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    VerticalTabsFullscreenDragTest,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse")));

#endif  // BUILDFLAG(IS_MAC)

#if BUILDFLAG(IS_LINUX)

// Variant of DetachToBrowserTabDragControllerTest that enables InitialWebUI
// with deferred BrowserView::Show() so that the detached browser is not
// immediately visible after Show() and TabDragController spins its
// kWaitingForWindowToShow nested RunLoop before entering the move loop.
class DetachTabWithDeferredShowDragControllerTest
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachTabWithDeferredShowDragControllerTest() {
    deferred_show_feature_list_.InitWithFeaturesAndParameters(
        {{features::kInitialWebUI, {}},
         {features::kWebUIReloadButton,
          {{features::kWebUIReloadButtonDeferBrowserViewShow.name, "true"}}},
         {features::kArtificialUIDelay,
          {{"initial_web_ui_delay_duration", "2s"},
           {"views_ui_delay_duration", "0ms"}}}},
        {});
  }

 private:
  base::test::ScopedFeatureList deferred_show_feature_list_;
};

// Detaches a tab into a new browser and, while the controller is waiting for
// the new browser's deferred Show() to complete, attempts to start a data drag
// from the source browser's window. The data drag must be rejected so that it
// cannot release the tab drag's pointer capture and take over the gesture.
IN_PROC_BROWSER_TEST_P(DetachTabWithDeferredShowDragControllerTest,
                       RejectDataDragWhileWaitingForDetachedWindow) {
  // The kWaitingForWindowToShow state is only reached on the RunMoveLoop path.
  if (!test::PlatformSupportsScreenCoordinates()) {
    GTEST_SKIP() << "RunMoveLoop is not used on this platform.";
  }

  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  aura::Window* const source_window = browser()->GetWindow()->GetNativeWindow();
  aura::Window* const source_root = source_window->GetRootWindow();

  bool data_drag_attempted = false;
  TabStrip* new_tab_strip = nullptr;
  // Releases the pointer once the controller has left kWaitingForWindowToShow
  // and entered the move loop, so the move loop sees the button-up.
  base::RepeatingClosure release_when_in_move_loop =
      base::BindLambdaForTesting([&]() {
        if (new_tab_strip && IsWaitingForWindowToShow(new_tab_strip)) {
          base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
              FROM_HERE, release_when_in_move_loop);
          return;
        }
        ASSERT_TRUE(ReleaseInput(0, /*async=*/true));
      });

  Tab* const tab = tab_strip->tab_at(0);
  ASSERT_TRUE(PressInputAtCenter(tab));
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      tab, base::BindLambdaForTesting([&]() {
        // The mouse-move that triggered detach has entered the
        // kWaitingForWindowToShow nested loop; post into it.
        base::SequencedTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE, base::BindLambdaForTesting([&]() {
              data_drag_attempted = true;
              ASSERT_TRUE(TabDragController::IsActive());
              ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
              BrowserWindowInterface* const new_browser =
                  ui_test_utils::GetBrowserNotInSet({browser()});
              ASSERT_TRUE(new_browser);
              new_tab_strip = GetTabStripForBrowser(new_browser);
              ASSERT_TRUE(IsWaitingForWindowToShow(new_tab_strip));

              auto data = std::make_unique<ui::OSExchangeData>();
              data->SetString(u"test");
              ui::mojom::DragOperation result =
                  aura::client::GetDragDropClient(source_root)
                      ->StartDragAndDrop(std::move(data), source_root,
                                         source_window, gfx::Point(),
                                         ui::DragDropTypes::DRAG_COPY,
                                         ui::mojom::DragEventSource::kMouse);
              EXPECT_EQ(ui::mojom::DragOperation::kNone, result);

              release_when_in_move_loop.Run();
            }));
      }),
      gfx::Vector2d(0, GetDetachY(tab_strip))));

  ASSERT_TRUE(
      base::test::RunUntil([]() { return !TabDragController::IsActive(); }));
  EXPECT_TRUE(data_drag_attempted);
}

INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachTabWithDeferredShowDragControllerTest,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse")));

#endif  // BUILDFLAG(IS_LINUX)

namespace {

// Invoked from the nested run loop.
void DragAllToSeparateWindowStep2(DetachToBrowserTabDragControllerTest* test,
                                  TabStrip* attached_tab_strip,
                                  TabStrip* target_tab_strip) {
  EXPECT_TRUE(IsDragSessionActive(attached_tab_strip));
  EXPECT_FALSE(IsDragSessionActive(target_tab_strip));
  EXPECT_TRUE(TabDragController::IsActive());
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Drag to target_tab_strip. This should stop the nested loop from dragging
  // the window.
  if (test::PlatformSupportsScreenCoordinates()) {
    const gfx::Rect old_window_bounds =
        attached_tab_strip->GetWidget()->GetWindowBoundsInScreen();
    const gfx::Rect target_bounds = target_tab_strip->GetBoundsInScreen();
    const gfx::Point target_point = test::DragPointInTabStrip(
        target_bounds, old_window_bounds, target_bounds.right() - 50);
    EXPECT_TRUE(target_bounds.Contains(target_point));
    EXPECT_TRUE(test->DragInputToAsync(target_point,
                                       test->GetWindowHint(target_tab_strip)));
  } else {
    const int offset_x = target_tab_strip->width() / 2 - 50;
    EXPECT_TRUE(test->DragInputToCenterAsync(target_tab_strip,
                                             gfx::Vector2d(offset_x, 0)));
  }
}

void DragAllToSeparateWindowCenterStep2(
    DetachToBrowserTabDragControllerTest* test,
    TabStrip* attached_tab_strip,
    TabStrip* target_tab_strip) {
  EXPECT_TRUE(IsDragSessionActive(attached_tab_strip));
  EXPECT_FALSE(IsDragSessionActive(target_tab_strip));
  EXPECT_TRUE(TabDragController::IsActive());
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Drag to target_tab_strip's center. This should stop the nested loop from
  // dragging the window.
  if (test::PlatformSupportsScreenCoordinates()) {
    const gfx::Rect old_window_bounds =
        attached_tab_strip->GetWidget()->GetWindowBoundsInScreen();
    const gfx::Rect target_bounds = target_tab_strip->GetBoundsInScreen();
    const gfx::Point target_point = test::DragPointInTabStrip(
        target_bounds, old_window_bounds, target_bounds.CenterPoint().x());
    EXPECT_TRUE(target_bounds.Contains(target_point));
    EXPECT_TRUE(test->DragInputToAsync(target_point,
                                       test->GetWindowHint(target_tab_strip)));
  } else {
    EXPECT_TRUE(test->DragInputToCenterAsync(target_tab_strip));
  }
}

}  // namespace

// Creates two browsers, then drags a combination of pinned and unpinned tabs
// from one to the other.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragPinnedAndUnpinnedToSeparateWindow) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  AddTabsAndResetBrowser(browser(), 1);
  browser()->GetTabStripModel()->SetTabPinned(0, true);
  StopAnimating(tab_strip);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  TabStripModel* model2 = browser2->GetTabStripModel();
  AddTabsAndResetBrowser(browser2, 1);
  ResetIDs(model2, 100);
  StopAnimating(tab_strip2);

  // Click the first tab and select the second tab so they are the only ones
  // selected.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  browser()->GetTabStripModel()->SelectTabAt(1);

  DragTabAndNotify(tab_strip, base::BindOnce(&DragAllToSeparateWindowStep2,
                                             this, tab_strip, tab_strip2));

  ASSERT_TRUE(WaitForAttach(tab_strip2, 4));

  // Drag to the trailing end of the tabstrip to ensure we're in a
  // predictable spot within the strip.
  StopAnimating(tab_strip2);
  ASSERT_TRUE(DragInputToCenter(tab_strip2->tab_at(3)));
  // Release mouse or touch, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());

  EXPECT_EQ("0 100 101 1", IDString(model2));
  EXPECT_TRUE(browser2->GetTabStripModel()->IsTabPinned(0));
  EXPECT_FALSE(browser2->GetTabStripModel()->IsTabPinned(1));
}

// Creates two browsers, then drags a split tab from one to the other.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragSplitTabToSeparateWindow) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  AddTabsAndResetBrowser(browser(), 1);
  browser()->GetTabStripModel()->ActivateTabAt(0);
  split_tabs::SplitTabId split_id =
      browser()->GetTabStripModel()->AddToNewSplit(
          {1}, split_tabs::SplitTabVisualData(),
          split_tabs::SplitTabCreatedSource::kToolbarButton);
  StopAnimating(tab_strip);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  TabStripModel* model2 = browser2->GetTabStripModel();
  AddTabsAndResetBrowser(browser2, 1);
  ResetIDs(model2, 100);
  StopAnimating(tab_strip2);

  DragTabAndNotify(tab_strip, base::BindOnce(&DragAllToSeparateWindowStep2,
                                             this, tab_strip, tab_strip2));

  ASSERT_TRUE(WaitForAttach(tab_strip2, 4));

  // Drag to the trailing end of the tabstrip to ensure we're in a
  // predictable spot within the strip.
  StopAnimating(tab_strip2);
  ASSERT_TRUE(DragInputToCenter(tab_strip2->tab_at(3)));
  // Release mouse or touch, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());

  EXPECT_EQ("100 101 0 1", IDString(model2));
  EXPECT_EQ(
      browser2->GetTabStripModel()->GetSplitData(split_id)->ListTabs().size(),
      2u);
}

// Flaky. http://crbug.com/40719820
#if BUILDFLAG(IS_MAC)
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/40734863
// These were flaking on all macs, so commented out ARCH_ above for
// crbug.com/40738482 too.
#define MAYBE_DragAllToSeparateWindow DISABLED_DragAllToSeparateWindow
#else
#define MAYBE_DragAllToSeparateWindow DragAllToSeparateWindow
#endif

// Creates two browsers, selects all tabs in first and drags into second.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragAllToSeparateWindow) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);

  browser()->GetTabStripModel()->SelectTabAt(0);

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip, base::BindOnce(&DragAllToSeparateWindowStep2,
                                             this, tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(IsDragSessionActive(tab_strip2));
  ASSERT_TRUE(TabDragController::IsActive());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Drag to the trailing end of the tabstrip to ensure we're in a
  // predictable spot within the strip.
  StopAnimating(tab_strip2);
  ASSERT_TRUE(DragInputToCenter(tab_strip2->tab_at(1)));
  // Release mouse or touch, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());

  ASSERT_FALSE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("100 0 1", IDString(browser2->GetTabStripModel()));

  EXPECT_FALSE(GetIsDragged(browser2));

  // Remaining browser window should not be maximized
  EXPECT_FALSE(browser2->GetWindow()->IsMaximized());
}

// Based on DragAllToSeparateWindow, which is flaky.
#if BUILDFLAG(IS_MAC)
#define MAYBE_DragAllToSeparateWindowThenDrop \
  DISABLED_DragAllToSeparateWindowThenDrop
#else
#define MAYBE_DragAllToSeparateWindowThenDrop DragAllToSeparateWindowThenDrop
#endif

// Creates two browsers, selects all tabs in first, drags into second, then
// drags out again and drops the new browser.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragAllToSeparateWindowThenDrop) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);
  ui_test_utils::WaitForBrowserSetLastActive(browser());

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  ui_test_utils::WaitForBrowserSetLastActive(browser2);
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);

  browser()->GetTabStripModel()->SelectTabAt(0);

  DragTabAndNotify(tab_strip, base::BindOnce(&DragAllToSeparateWindowStep2,
                                             this, tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(IsDragSessionActive(tab_strip2));
  ASSERT_TRUE(TabDragController::IsActive());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  // A new browser should open to hold the dragged tabs. We use a custom
  // AsyncBrowserWaiter because waiting synchronously via BrowserCreatedObserver
  // here would hijack the thread before TabDragController::RunMoveLoop() can
  // spin up its own nested loop, causing deadlocks on Wayland and X11.
  BrowserWindowInterface* new_browser = nullptr;
  TabStrip* new_tab_strip = nullptr;

  AsyncBrowserWaiter waiter(
      base::BindLambdaForTesting([&](BrowserWindowInterface* browser) {
        new_browser = browser;
        new_tab_strip = GetTabStripForBrowser(browser);

        ASSERT_FALSE(IsDragSessionActive(tab_strip2));
        ASSERT_TRUE(IsDragSessionActive(new_tab_strip));
        ASSERT_TRUE(TabDragController::IsActive());

#if BUILDFLAG(IS_WIN)
        ReleaseInput(0, /*async=*/true);
#else
        ReleaseInput();
#endif
      }));

  // Drag out of tab_strip2 again.
  EXPECT_TRUE(DragInputToCenterAsync(tab_strip2,
                                     /*offset=*/{0, GetDetachY(tab_strip2)}));

  ASSERT_TRUE(base::test::RunUntil([&]() {
    return new_tab_strip && !IsDragSessionActive(new_tab_strip) &&
           !TabDragController::IsActive();
  }));
  EXPECT_EQ("100", IDString(browser2->GetTabStripModel()));
  EXPECT_EQ("0 1", IDString(new_browser->GetTabStripModel()));
  EXPECT_FALSE(GetIsDragged(new_browser));
}

namespace {

// Invoked from the nested run loop.
void DoubleNestedRunLoopStep2(DetachToBrowserTabDragControllerTest* test,
                              TabStrip* attached_tab_strip,
                              TabStrip* target_tab_strip) {
  EXPECT_TRUE(IsDragSessionActive(attached_tab_strip));
  EXPECT_FALSE(IsDragSessionActive(target_tab_strip));
  EXPECT_TRUE(TabDragController::IsActive());
  EXPECT_TRUE(
      TabDragController::IsAttachedTo(attached_tab_strip->GetDragContext()));
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  TabDragController* const drag_controller =
      attached_tab_strip->GetDragContext()->GetDragController();
  const gfx::Point target_center =
      GetCenterInScreenCoordinates(target_tab_strip);

  // Drag to target_tab_strip. This should cause TabDragController to ask to end
  // the nested run loop. Normally, we'd return from here to allow the nested
  // loop to exit, but to reproduce the conditions for the crash, we won't.
  ASSERT_EQ(drag_controller->Drag(target_center),
            TabDragController::Liveness::kAlive);

  // Call Drag directly - still on the nested run loop! - in a way that would
  // spawn a nested run loop if processed.
  ASSERT_EQ(drag_controller->Drag(
                target_center + gfx::Vector2d(0, GetDetachY(target_tab_strip))),
            TabDragController::Liveness::kAlive);

  // Release input to ensure the nested run loop does actually exit.
  EXPECT_TRUE(test->ReleaseInput(0, /*async=*/true));
}

}  // namespace

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragToSeparateWindowAttemptToSpawnDoubleNestedRunLoop) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Wayland doesn't necessarily support window move loops; this test doesn't
  // make sense in that case.
  if (!tab_strip->GetWidget()->IsMoveLoopSupported()) {
    return;
  }

  AddTabsAndResetBrowser(browser(), 1);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);

  browser()->GetTabStripModel()->SelectTabAt(0);

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip, base::BindOnce(&DoubleNestedRunLoopStep2, this,
                                             tab_strip, tab_strip2));

  // The drag should have ended.
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(TabDragController::IsActive());
}

#if BUILDFLAG(IS_MAC) && defined(ARCH_CPU_ARM64)
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/40734863
#define MAYBE_DragWindowIntoGroup DISABLED_DragWindowIntoGroup
#else
#define MAYBE_DragWindowIntoGroup DragWindowIntoGroup
#endif

// Creates two browser with two tabs each. The first browser has two tabs not in
// any group. The second browser {browser2} has two tabs in another group
// {group1}. Dragging the two tabs in the first browser into the middle of the
// second browser will insert the two dragged tabs into the {group1}} after the
// first tab.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragWindowIntoGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);
  StopAnimating(tab_strip);

  // Set up the second browser with two tabs in a group with distinct IDs.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  TabStripModel* model2 = browser2->GetTabStripModel();
  AddTabsAndResetBrowser(browser2, 1);
  ResetIDs(model2, 100);
  tab_groups::TabGroupId group1 = model2->AddToNewGroup({0, 1});
  StopAnimating(tab_strip2);

  // Click the first tab and select the second tab so they are the only ones
  // selected.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(ReleaseInput());
  browser()->GetTabStripModel()->SelectTabAt(1);

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DragAllToSeparateWindowCenterStep2, this,
                                  tab_strip, tab_strip2));

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());

  EXPECT_EQ("100 0 1 101", IDString(model2));
  EXPECT_EQ(model2->group_model()->GetTabGroup(group1)->ListTabs(),
            gfx::Range(0, 4));
}

// Creates two browsers, then drags a group from one to the other.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragGroupHeaderToSeparateWindow) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 1);
  tab_groups::TabGroupId group = model->AddToNewGroup({0, 1});
  tab_groups::TabGroupColorId group_color = tab_strip->GetGroupColorId(group);
  StopAnimating(tab_strip);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  TabStripModel* model2 = browser2->GetTabStripModel();
  StopAnimating(tab_strip2);

  // Drag the group by its header into the second browser.
  DragToDetachGroupAndNotify(tab_strip,
                             base::BindOnce(&DragAllToSeparateWindowStep2, this,
                                            tab_strip, tab_strip2),
                             group);

  ASSERT_TRUE(WaitForAttach(tab_strip2, 3));
  ASSERT_TRUE(ReleaseInput());

  // Expect the group to be in browser2, but with a new tab_groups::TabGroupId.
  EXPECT_EQ("100 0 1", IDString(model2));
  EXPECT_EQ(model2->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(1, 3));
  EXPECT_EQ(tab_strip2->GetGroupColorId(group), group_color);
}

// Creates two browsers, then drags a group from one to the other.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragGroupHeaderWithSplitToSeparateWindow) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 1);
  tab_groups::TabGroupId group = model->AddToNewGroup({0, 1});

  model->ActivateTabAt(0);
  model->AddToNewSplit({1}, split_tabs::SplitTabVisualData(),
                       split_tabs::SplitTabCreatedSource::kToolbarButton);

  tab_groups::TabGroupColorId group_color = tab_strip->GetGroupColorId(group);
  StopAnimating(tab_strip);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  TabStripModel* model2 = browser2->GetTabStripModel();
  StopAnimating(tab_strip2);

  // Drag the group by its header into the second browser.
  DragToDetachGroupAndNotify(tab_strip,
                             base::BindOnce(&DragAllToSeparateWindowStep2, this,
                                            tab_strip, tab_strip2),
                             group);

  ASSERT_TRUE(WaitForAttach(tab_strip2, 3));
  ASSERT_TRUE(ReleaseInput());

  // Expect the group to be in browser2.
  EXPECT_EQ("100 0 1", IDString(model2));
  EXPECT_EQ(model2->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(1, 3));
  EXPECT_EQ(tab_strip2->GetGroupColorId(group), group_color);
}

// Drags a tab group by the header to a new position toward the right and
// presses escape to revert the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertHeaderDragRight) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({0, 1});
  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  TabGroupHeader* group_header = tab_strip->group_header(group);
  EXPECT_FALSE(group_header->dragging());
  ASSERT_TRUE(PressInputAtCenter(group_header));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(group_header->dragging());

  ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
      browser()->GetWindow()->GetNativeWindow(), ui::VKEY_ESCAPE, false, false,
      false, false));
  StopAnimating(tab_strip);

  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_FALSE(group_header->dragging());
  EXPECT_EQ("0 1 2 3", IDString(browser()->GetTabStripModel()));
  std::vector<tab_groups::TabGroupId> groups =
      model->group_model()->ListTabGroups();
  EXPECT_EQ(1u, groups.size());
  EXPECT_EQ(model->group_model()->GetTabGroup(groups[0])->ListTabs(),
            gfx::Range(0, 2));
}

// Drags a tab within a tab group and presses escape to revert the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertTabDragWithinGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({0, 1, 2});
  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  // Drag the first tab in the group to the end of the group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(2)));

  ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
      browser()->GetWindow()->GetNativeWindow(), ui::VKEY_ESCAPE, false, false,
      false, false));
  StopAnimating(tab_strip);

  EXPECT_EQ("0 1 2 3", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(0, 3));
}

// Drags a tab from a tab group to after the group and presses escape to revert
// the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertTabDragOutOfGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({0, 1, 2});
  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  // Drag the first tab in the group to after the group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(3)));

  ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
      browser()->GetWindow()->GetNativeWindow(), ui::VKEY_ESCAPE, false, false,
      false, false));
  StopAnimating(tab_strip);

  EXPECT_EQ("0 1 2 3", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(0, 3));
}

// Drags multiple tabs from a tab group and presses escape to revert the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertMultipleTabsInGroupDrag) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({0, 1, 2});
  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  // Select tabs 0 and 1.
  ui::ListSelectionModel selection;
  selection.AddIndexToSelection(0);
  selection.AddIndexToSelection(1);
  selection.set_active(0);
  model->SetSelectionFromModel(selection);

  // Drag the selected tabs to after the group.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(3)));

  ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
      browser()->GetWindow()->GetNativeWindow(), ui::VKEY_ESCAPE, false, false,
      false, false));
  StopAnimating(tab_strip);

  EXPECT_EQ("0 1 2 3", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(0, 3));
}

// escape to revert the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertHeaderDragLeft) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({2, 3});
  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  TabGroupHeader* group_header = tab_strip->group_header(group);
  EXPECT_FALSE(group_header->dragging());
  ASSERT_TRUE(PressInputAtCenter(group_header));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(group_header->dragging());

  ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
      browser()->GetWindow()->GetNativeWindow(), ui::VKEY_ESCAPE, false, false,
      false, false));

  StopAnimating(tab_strip);
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_FALSE(group_header->dragging());
  EXPECT_EQ("0 1 2 3", IDString(browser()->GetTabStripModel()));
  std::vector<tab_groups::TabGroupId> groups =
      model->group_model()->ListTabGroups();
  EXPECT_EQ(1u, groups.size());
  EXPECT_EQ(model->group_model()->GetTabGroup(groups[0])->ListTabs(),
            gfx::Range(2, 4));
}

namespace {

class AsyncEscapePresser : public views::WidgetObserver {
 public:
  explicit AsyncEscapePresser(views::Widget* widget) : widget_(widget) {
    if (widget_->IsVisible()) {
      PressEscape();
    } else {
      widget_->AddObserver(this);
    }
  }

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

  ~AsyncEscapePresser() override = default;

  void OnWidgetVisibilityChanged(views::Widget* widget, bool visible) override {
    if (visible && widget_ == widget) {
      widget_->RemoveObserver(this);
      PressEscape();
    }
  }

  void OnWidgetDestroying(views::Widget* widget) override {
    if (widget_ == widget) {
      widget_->RemoveObserver(this);
      widget_ = nullptr;
      base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE,
                                                                 this);
    }
  }

 private:
  void PressEscape() {
    ui_controls::SendKeyPress(widget_->GetNativeWindow(), ui::VKEY_ESCAPE,
                              false, false, false, false);
    widget_ = nullptr;
    base::SequencedTaskRunner::GetCurrentDefault()->DeleteSoon(FROM_HERE, this);
  }

  raw_ptr<views::Widget> widget_;
};

void PressEscapeWhileDetachedHeaderStep2(
    DetachToBrowserTabDragControllerTest* test,
    BrowserWindowInterface* new_browser) {
  if (!new_browser) {
    new_browser = ui_test_utils::GetBrowserNotInSet({test->browser()});
  }
  CHECK(new_browser);
  // At this moment there should be a new browser window for the dragged tabs.
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  std::vector<tab_groups::TabGroupId> new_browser_groups =
      new_browser->GetTabStripModel()->group_model()->ListTabGroups();
  EXPECT_EQ(1u, new_browser_groups.size());
  EXPECT_EQ(0u, test->browser()
                    ->GetTabStripModel()
                    ->group_model()
                    ->ListTabGroups()
                    .size());

  TabGroupHeader* new_group_header =
      GetTabStripForBrowser(new_browser)->group_header(new_browser_groups[0]);
  EXPECT_TRUE(new_group_header->dragging());

  // Wait asynchronously for the new browser window to be visible before
  // pressing ESC, ensuring we don't dispatch it blindly into the void while the
  // window is still waiting for WebUI to initialize and defers `RunMoveLoop()`.
  new AsyncEscapePresser(
      BrowserView::GetBrowserViewForBrowser(new_browser)->GetWidget());
}

}  // namespace

// Drags a tab group by the header and while detached presses escape to revert
// the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertHeaderDragWhileDetached) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 1);
  tab_groups::TabGroupId group = model->AddToNewGroup({0});
  StopAnimating(tab_strip);
  EnsureFocusToTabStrip(tab_strip);

  ui_test_utils::BrowserDestroyedObserver browser_removed_observer;
  AsyncBrowserWaiter waiter(
      base::BindLambdaForTesting([this](BrowserWindowInterface* new_browser) {
        PressEscapeWhileDetachedHeaderStep2(this, new_browser);
      }));
  DragToDetachGroupAndNotify(tab_strip, base::DoNothing(), group);
  // Ensure completion of asynchronous browser closure.
  browser_removed_observer.Wait();

  EXPECT_FALSE(tab_strip->group_header(group)->dragging());

  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ("0 1", IDString(browser()->GetTabStripModel()));
  std::vector<tab_groups::TabGroupId> groups =
      model->group_model()->ListTabGroups();
  EXPECT_EQ(1u, groups.size());
  EXPECT_EQ(model->group_model()->GetTabGroup(groups[0])->ListTabs(),
            gfx::Range(0, 1));
}

// Creates a browser with four tabs where the second and third tab is in a
// collapsed group. Drag the fourth tab to the left past the group header. The
// fourth tab should swap places with the collapsed group header.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragTabLeftPastCollapsedGroupHeader) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({1, 2});
  StopAnimating(tab_strip);

  EXPECT_EQ(4, model->count());
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(1, 3));
  EXPECT_FALSE(model->IsGroupCollapsed(group));
  tab_strip->ToggleTabGroupCollapsedState(group);
  StopAnimating(tab_strip);
  EXPECT_TRUE(model->IsGroupCollapsed(group));

  // Dragging the last tab to the left should cause it to swap places with the
  // collapsed group header.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(3)));
  ASSERT_TRUE(
      DragInputTo(test::GetLeftCenterInScreenCoordinates(tab_strip->tab_at(3)),
                  GetWindowHint(tab_strip)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("0 3 1 2", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(2, 4));
  EXPECT_TRUE(model->IsGroupCollapsed(group));
}

// Creates a browser with four tabs where the second and third tab is in a
// collapsed group. Drag the first tab to the right past the group header. The
// first tab should swap places with the collapsed group header.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragTabRightPastCollapsedGroupHeader) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({1, 2});
  StopAnimating(tab_strip);

  EXPECT_EQ(4, model->count());
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(1, 3));
  EXPECT_FALSE(model->IsGroupCollapsed(group));
  tab_strip->ToggleTabGroupCollapsedState(group);
  StopAnimating(tab_strip);
  EXPECT_TRUE(model->IsGroupCollapsed(group));

  // Dragging the first tab to the right should cause it to swap places with the
  // collapsed group header.
  const gfx::NativeWindow window_hint = GetWindowHint(tab_strip);
  ASSERT_TRUE(
      PressInput(test::GetLeftCenterInScreenCoordinates(tab_strip->tab_at(0)),
                 window_hint));
  ASSERT_TRUE(
      DragInputTo(test::GetRightCenterInScreenCoordinates(tab_strip->tab_at(0)),
                  window_hint));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  EXPECT_EQ("1 2 0 3", IDString(model));
  EXPECT_EQ(model->group_model()->GetTabGroup(group)->ListTabs(),
            gfx::Range(0, 2));
  EXPECT_TRUE(model->IsGroupCollapsed(group));
}

// Drags a tab group by the header and while detached presses escape to revert
// the drag.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       RevertCollapsedHeaderDragWhileDetached) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 1);
  tab_groups::TabGroupId group = model->AddToNewGroup({0});
  EXPECT_FALSE(model->IsGroupCollapsed(group));
  EnsureFocusToTabStrip(tab_strip);

  tab_strip->ToggleTabGroupCollapsedState(group);
  StopAnimating(tab_strip);
  EXPECT_TRUE(model->IsGroupCollapsed(group));

  ui_test_utils::BrowserDestroyedObserver browser_destroyed_observer;
  AsyncBrowserWaiter waiter(
      base::BindLambdaForTesting([this](BrowserWindowInterface* new_browser) {
        PressEscapeWhileDetachedHeaderStep2(this, new_browser);
      }));
  DragToDetachGroupAndNotify(tab_strip, base::DoNothing(), group);
  // Ensure completion of asynchronous browser closure.
  browser_destroyed_observer.Wait();

  EXPECT_FALSE(tab_strip->group_header(group)->dragging());
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ("0 1", IDString(browser()->GetTabStripModel()));
  std::vector<tab_groups::TabGroupId> groups =
      model->group_model()->ListTabGroups();
  EXPECT_EQ(1u, groups.size());
  EXPECT_EQ(model->group_model()->GetTabGroup(groups[0])->ListTabs(),
            gfx::Range(0, 1));
  EXPECT_TRUE(tab_strip->IsGroupCollapsed(group));
}

// Creates a browser with four tabs. The first two tabs belong in Tab Group 1.
// Dragging the collapsed group header of Tab Group 1 will not expand the
// group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragCollapsedGroupHeaderRemainsCollapsed) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 3);
  tab_groups::TabGroupId group = model->AddToNewGroup({2, 3});
  ASSERT_FALSE(model->IsGroupCollapsed(group));
  tab_strip->ToggleTabGroupCollapsedState(group);
  StopAnimating(tab_strip);
  ASSERT_TRUE(model->IsGroupCollapsed(group));
  EnsureFocusToTabStrip(tab_strip);

  ASSERT_EQ(4, model->count());
  ASSERT_EQ(2u, group_model->GetTabGroup(group)->ListTabs().length());

  // Drag group1, group should remain collapsed.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->group_header(group)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(TabDragController::IsActive());
  EXPECT_TRUE(model->IsGroupCollapsed(group));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);
  EXPECT_TRUE(model->IsGroupCollapsed(group));
}

#if BUILDFLAG(IS_MAC)
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/40734863
// Test is flaky on Mac: https://crbug.com/40742603
#define MAYBE_DragCollapsedGroupHeaderToSeparateWindow \
  DISABLED_DragCollapsedGroupHeaderToSeparateWindow
#else
#define MAYBE_DragCollapsedGroupHeaderToSeparateWindow \
  DragCollapsedGroupHeaderToSeparateWindow
#endif

// Creates two browsers, then drags a collapsed group from one to the other.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragCollapsedGroupHeaderToSeparateWindow) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 2);
  tab_groups::TabGroupId group = model->AddToNewGroup({0, 1});
  EXPECT_FALSE(model->IsGroupCollapsed(group));
  tab_strip->ToggleTabGroupCollapsedState(group);
  StopAnimating(tab_strip);
  EXPECT_TRUE(model->IsGroupCollapsed(group));

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  TabStripModel* model2 = browser2->GetTabStripModel();
  StopAnimating(tab_strip2);

  // Drag the group by its header into the second browser.
  TabGroupHeader* group_header = tab_strip->group_header(group);
  ASSERT_TRUE(PressInputAtCenter(group_header));
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      group_header,
      base::BindOnce(&DragToSeparateWindowStep2, this, browser(), browser2),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  test::BrowserChangeWaiter(test::BrowserChangeWaiter::ChangeType::kRemoved)
      .Wait(base::BindLambdaForTesting(
          [this]() { ASSERT_TRUE(ReleaseInput()); }));

  // Expect the group to be in browser2, but with a new tab_groups::TabGroupId
  // and not collapsed.
  EXPECT_EQ("100 0 1", IDString(model2));
  std::vector<tab_groups::TabGroupId> browser2_groups =
      model2->group_model()->ListTabGroups();
  EXPECT_EQ(1u, browser2_groups.size());
  EXPECT_EQ(model2->group_model()->GetTabGroup(browser2_groups[0])->ListTabs(),
            gfx::Range(1, 3));
  ASSERT_FALSE(tab_strip2->IsGroupCollapsed(browser2_groups[0]));
  EXPECT_EQ(browser2_groups[0], group);
}

// Detachable tabs are not supported for PWAs on Mac so these tests don't apply.
#if !BUILDFLAG(IS_MAC)
using DetachTabWithUrlControlledByWebApp = DetachToBrowserTabDragControllerTest;
// Test tearing off a tab displaying a url controlled by a web app.
// The kTearOffWebAppTabOpensWebAppWindow experiment determines whether the new
// browser window will be a normal browser window or an app window.
IN_PROC_BROWSER_TEST_P(DetachTabWithUrlControlledByWebApp, TearOffWebApp) {
  // OS integration is needed to be able to launch web applications. This
  // override ensures OS integration doesn't leave any traces.
  std::unique_ptr<web_app::OsIntegrationTestOverrideImpl::BlockingRegistration>
      override_registration;

  {
    base::ScopedAllowBlockingForTesting allow_blocking;
    override_registration =
        web_app::OsIntegrationTestOverrideImpl::OverrideForTesting();
  }

  // Install tabbed web app.
  auto web_app_info = web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
      GURL("https://www.example.com"));
  web_app_info->title = u"A tabbed web app";
  web_app_info->user_display_mode =
      web_app::mojom::UserDisplayMode::kStandalone;
  webapps::AppId app_id = web_app::test::InstallWebApp(browser()->GetProfile(),
                                                       std::move(web_app_info));

  // Load URL controlled by installed web app.
  AddTabsAndResetBrowser(browser(), 1, GURL("https://www.example.com/"));

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  EXPECT_EQ(browser()->GetTabStripModel()->count(), 2);

  // Move to the second tab and drag it enough that it detaches.
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(),
      base::BindOnce(&DetachToBrowserTabDragControllerTest::
                         ReleaseInputAfterWindowDetached,
                     base::Unretained(this), tab_strip->tab_at(1)->width()),
      1);
  ASSERT_TRUE(new_browser);

  // Expect first window is left with just the start tab.
  TabStripModel* source_tab_strip_model = browser()->GetTabStripModel();
  EXPECT_EQ(source_tab_strip_model->count(), 1);
  EXPECT_FALSE(source_tab_strip_model->IsTabPinned(0));
  EXPECT_EQ(source_tab_strip_model->GetWebContentsAt(0)->GetVisibleURL(),
            GURL("about:blank"));

  // Expect the newly created window has the dragged tab.
  TabStripModel* dest_tab_strip_model = new_browser->GetTabStripModel();
  EXPECT_EQ(dest_tab_strip_model->count(), 1);
  EXPECT_EQ(dest_tab_strip_model->GetWebContentsAt(0)->GetVisibleURL(),
            web_app::WebAppProvider::GetForTest(browser()->GetProfile())
                ->registrar_unsafe()
                .GetAppStartUrl(app_id));
  EXPECT_EQ(dest_tab_strip_model->active_index(), 0);

  // Check that right type of browser window is opened, depending on the value
  // of kTearOffWebAppTabOpensWebAppWindow experiment.
  EXPECT_EQ(new_browser->GetType(),
            std::get<0>(GetParam())
                ? BrowserWindowInterface::Type::TYPE_APP
                : BrowserWindowInterface::Type::TYPE_NORMAL);
}

INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachTabWithUrlControlledByWebApp,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Bool(),
        /*input_source=*/::testing::Values("mouse")));

class DetachToBrowserTabDragControllerTestWithTabbedWebApp
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachToBrowserTabDragControllerTestWithTabbedWebApp() {
    scoped_feature_list_.InitWithFeatures(
        {blink::features::kDesktopPWAsTabStrip}, {});
  }

  webapps::AppId InstallMockApp(bool add_home_tab) {
    auto web_app_info =
        web_app::WebAppInstallInfo::CreateWithStartUrlForTesting(
            GURL("https://www.example.com"));
    web_app_info->title = u"A tabbed web app";
    web_app_info->user_display_mode =
        web_app::mojom::UserDisplayMode::kStandalone;
    web_app_info->display_override = {
        web_app::DisplayOverride::Create(blink::mojom::DisplayMode::kTabbed)};
    if (add_home_tab) {
      blink::Manifest::TabStrip manifest_tab_strip;
      manifest_tab_strip.home_tab = blink::Manifest::HomeTabParams();
      web_app_info->tab_strip = std::move(manifest_tab_strip);
    }

    return web_app::test::InstallWebApp(browser()->GetProfile(),
                                        std::move(web_app_info));
  }

 private:
  web_app::OsIntegrationTestOverrideBlockingRegistration faked_os_integration_;
  base::test::ScopedFeatureList scoped_feature_list_;
};

// Tabbed web apps with the home tab cannot have detachable tabs.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithTabbedWebApp,
                       HomeTabAddedToEveryWindow) {
  // Install tabbed web app.
  webapps::AppId app_id = InstallMockApp(/*add_home_tab=*/true);
  BrowserWindowInterface* const app_browser =
      web_app::LaunchWebAppBrowser(browser()->GetProfile(), app_id);
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Close normal browser since other code expects only 1 browser to start.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  SetBrowser(app_browser);
  ASSERT_EQ(app_browser, browser());

  AddTabsAndResetBrowser(browser(), 1, GURL("https://www.example.com/newpage"));

  TabStrip* tab_strip = GetTabStripForBrowser(app_browser);
  EXPECT_EQ(browser()->GetTabStripModel()->count(), 2);

  // Move to the second tab and drag it enough that it detaches.
  int tab_1_width = tab_strip->tab_at(1)->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(),
      base::BindOnce(&DetachToBrowserTabDragControllerTest::
                         ReleaseInputAfterWindowDetached,
                     base::Unretained(this), tab_1_width),
      1);
  ASSERT_TRUE(new_browser);

  // Expect first window is left with just the home tab.
  TabStripModel* source_tab_strip_model = browser()->GetTabStripModel();
  EXPECT_EQ(source_tab_strip_model->count(), 1);
  EXPECT_TRUE(source_tab_strip_model->IsTabPinned(0));
  EXPECT_EQ(source_tab_strip_model->GetWebContentsAt(0)->GetVisibleURL(),
            web_app::WebAppProvider::GetForTest(browser()->GetProfile())
                ->registrar_unsafe()
                .GetAppStartUrl(app_id));

  // Expect the newly created window has the dragged tab and a home tab.
  TabStripModel* dest_tab_strip_model = new_browser->GetTabStripModel();
  EXPECT_EQ(dest_tab_strip_model->count(), 2);
  EXPECT_TRUE(dest_tab_strip_model->IsTabPinned(0));
  EXPECT_EQ(dest_tab_strip_model->GetWebContentsAt(0)->GetVisibleURL(),
            source_tab_strip_model->GetWebContentsAt(0)->GetVisibleURL());
  EXPECT_EQ(dest_tab_strip_model->GetWebContentsAt(1)->GetVisibleURL(),
            GURL("https://www.example.com/newpage"));
  EXPECT_EQ(dest_tab_strip_model->active_index(), 1);
}

// Home tab can't be detached.
// TODO(crbug.com/40245163): Enable this test for Linux.
#if BUILDFLAG(IS_LINUX)
#define MAYBE_CantDragHomeTab DISABLED_CantDragHomeTab
#else
#define MAYBE_CantDragHomeTab CantDragHomeTab
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithTabbedWebApp,
                       MAYBE_CantDragHomeTab) {
  // Install tabbed web app.
  webapps::AppId app_id = InstallMockApp(/*add_home_tab=*/true);
  BrowserWindowInterface* const app_browser =
      web_app::LaunchWebAppBrowser(browser()->GetProfile(), app_id);
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Close normal browser since other code expects only 1 browser to start.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  SetBrowser(app_browser);
  ASSERT_EQ(app_browser, browser());

  AddTabsAndResetBrowser(browser(), 1, GURL("https://www.example.com/newpage"));

  TabStrip* tab_strip = GetTabStripForBrowser(app_browser);
  EXPECT_EQ(browser()->GetTabStripModel()->count(), 2);

  // Try dragging the home tab enough that it would usually detach.
  const Tab* tab = tab_strip->tab_at(0);
  ASSERT_TRUE(PressInputAtCenter(tab));
  ASSERT_TRUE(DragInputToCenter(tab, gfx::Vector2d(0, GetDetachY(tab_strip))));

  ASSERT_TRUE(TabDragController::IsActive());

  ASSERT_TRUE(ReleaseInput());

  // There should only be one browser window containing two tabs.
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ(browser()->GetTabStripModel()->count(), 2);
}

// Tabbed web apps without a home tab do not have home tab added.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithTabbedWebApp,
                       NoHomeTab) {
  // Install tabbed web app.
  webapps::AppId app_id = InstallMockApp(/*add_home_tab=*/false);
  BrowserWindowInterface* const app_browser =
      web_app::LaunchWebAppBrowser(browser()->GetProfile(), app_id);
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Close normal browser since other code expects only 1 browser to start.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  SetBrowser(app_browser);
  ASSERT_EQ(app_browser, browser());

  AddTabsAndResetBrowser(browser(), 1, GURL("https://www.example.com/newpage"));

  TabStrip* tab_strip = GetTabStripForBrowser(app_browser);
  EXPECT_EQ(browser()->GetTabStripModel()->count(), 2);

  // Move to the second tab and drag it enough that it detaches.
  int tab_1_width = tab_strip->tab_at(1)->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(),
      base::BindOnce(&DetachToBrowserTabDragControllerTest::
                         ReleaseInputAfterWindowDetached,
                     base::Unretained(this), tab_1_width),
      1);

  // Expect 2 app windows with 1 tab each.
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ(browser()->GetTabStripModel()->count(), 1);
  EXPECT_EQ(new_browser->GetTabStripModel()->count(), 1);
}
#endif  // !BUILDFLAG(IS_MAC)

// Creates two browsers, selects all tabs in first, drags into second, then hits
// escape.
//
// Escape does not revert this drag: dragging every tab out clears
// `source_context_`, and EndDrag() treats a cancel as a revert only while that
// context exists. The drag completes in place instead.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragAllToSeparateWindowAndCancel) {
#if BUILDFLAG(IS_OZONE)
  if (::ui::OzonePlatform::RunningOnWaylandForTest()) {
    GTEST_SKIP() << "Wayland-chrome doesn't cancel tab dragging on esc.";
  }
#endif
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);

  browser()->GetTabStripModel()->SelectTabAt(0);

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip, base::BindOnce(&DragAllToSeparateWindowStep2,
                                             this, tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(IsDragSessionActive(tab_strip2));
  ASSERT_TRUE(WaitForAttach(tab_strip2, 3));
  ASSERT_TRUE(TabDragController::IsActive());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  // `source_context_` decides revert-versus-complete and is cleared from a
  // TabStripModelObserver callback, so escape races it.
  ASSERT_TRUE(base::test::RunUntil(
      [&]() { return TabDragController::GetSourceContext() == nullptr; }));
  ASSERT_TRUE(TabDragController::IsActive());

  // Target the window holding capture, not whichever browser is focused.
  ASSERT_TRUE(ui_test_utils::SendKeyPressToWindowSync(
      browser2->GetWindow()->GetNativeWindow(), ui::VKEY_ESCAPE, false, false,
      false, false));

  ASSERT_TRUE(
      base::test::RunUntil([&]() { return !TabDragController::IsActive(); }));
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("100 0 1", IDString(browser2->GetTabStripModel()));

  // browser() will have been destroyed, but browser2 should remain.
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  EXPECT_FALSE(GetIsDragged(browser2));

  // Remaining browser window should not be maximized
  EXPECT_FALSE(browser2->GetWindow()->IsMaximized());
}

// Creates two browsers, selects all tabs in first, drags into second, then hits
// escape.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragAllToSeparateWindowWithPinnedTabs) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Open a second tab.
  AddTabsAndResetBrowser(browser(), 1);
  // Re-select the first one.
  browser()->GetTabStripModel()->SelectTabAt(0);

  // Create another browser.
  BrowserWindowInterface* target_browser = CreateAnotherBrowserAndResize();
  TabStrip* target_tab_strip = GetTabStripForBrowser(target_browser);

  // Pin the tab in the target tabstrip.
  target_browser->GetTabStripModel()->SetTabPinned(0, true);
  StopAnimating(target_tab_strip);

  // Drag the selected tabs to `target_tab_strip`.
  DragTabAndNotify(
      tab_strip, base::BindOnce(&DragAllToSeparateWindowStep2, this, tab_strip,
                                target_tab_strip));

  // Should now be attached to `target_tab_strip`.
  ASSERT_TRUE(IsDragSessionActive(target_tab_strip));
  ASSERT_TRUE(WaitForAttach(target_tab_strip, 3));
  ASSERT_TRUE(TabDragController::IsActive());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Drag to the trailing end of the tabstrip to ensure we're in a consistent
  // spot within the strip.
  StopAnimating(target_tab_strip);
  ASSERT_TRUE(DragInputToCenter(target_tab_strip->tab_at(1)));

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());
  ASSERT_FALSE(IsDragSessionActive(target_tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("100 0 1", IDString(target_browser->GetTabStripModel()));

  EXPECT_FALSE(GetIsDragged(target_browser));

  // Remaining browser window should not be maximized
  EXPECT_FALSE(target_browser->GetWindow()->IsMaximized());
}

#if BUILDFLAG(IS_CHROMEOS) || BUILDFLAG(IS_LINUX)
// Flaky on ChromeOS.
#define MAYBE_DragDirectlyToSecondWindow DISABLED_DragDirectlyToSecondWindow
#else
#define MAYBE_DragDirectlyToSecondWindow DragDirectlyToSecondWindow
#endif
// Creates two browsers, drags from first into the second in such a way that
// no detaching should happen.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragDirectlyToSecondWindow) {
  // TODO(pkasting): Crashes when detaching browser.  https://crbug.com/41433817
  if (input_source() == InputSource::INPUT_SOURCE_TOUCH) {
    VLOG(1) << "Test is DISABLED for touch input.";
    return;
  }

  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  const gfx::Rect initial_bounds(browser2->GetWindow()->GetBounds());

  // Place the first browser directly below the second in such a way that
  // dragging a tab upwards will drag it directly into the second browser's
  // tabstrip.
  BrowserView* const browser_view2 =
      BrowserView::GetBrowserViewForBrowser(browser2);
  browser_view2->GetWidget()->LayoutRootViewIfNecessary();
  auto* const tabstrip = browser_view2->tab_strip_view()->GetTabStripView();
  gfx::Rect tabstrip_bounds = tabstrip->GetLocalBounds();
  tabstrip_bounds = tabstrip->ConvertRectToWidget(tabstrip_bounds);
  gfx::Rect bounds = initial_bounds;
  bounds.Offset(0, tabstrip_bounds.bottom());
  browser()->GetWindow()->SetBounds(bounds);

  // Ensure the first browser is on top so clicks go to it.
  ui_test_utils::BrowserActivationWaiter activation_waiter(browser());
  browser()->GetWindow()->Activate();
  activation_waiter.WaitForActivation();

  // Move to the first tab and drag it to browser2.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));

  const views::View* tab = tab_strip2->tab_at(0);
  ASSERT_TRUE(DragInputToCenter(tab, gfx::Vector2d(-tab->width() / 4, 0)));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_TRUE(TabDragController::IsActive());

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 100", IDString(browser2->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));

  // Make sure that the window is still managed and not user moved.
  EXPECT_TRUE(
      IsWindowPositionManaged(browser2->GetWindow()->GetNativeWindow()));
  EXPECT_FALSE(HasUserChangedWindowPositionOrSize(
      browser2->GetWindow()->GetNativeWindow()));

  // Also make sure that the drag to window position has not changed.
  EXPECT_EQ(initial_bounds.ToString(),
            browser2->GetWindow()->GetBounds().ToString());
}

// Flaky. https://crbug.com/40748225
#if BUILDFLAG(IS_MAC) && defined(ARCH_CPU_ARM64)
// Bulk-disabled for arm64 bot stabilization: https://crbug.com/40734863
#define MAYBE_DragSingleTabToSeparateWindow \
  DISABLED_DragSingleTabToSeparateWindow
#else
#define MAYBE_DragSingleTabToSeparateWindow DragSingleTabToSeparateWindow
#endif

// Creates two browsers, the first browser has a single tab and drags into the
// second browser.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_DragSingleTabToSeparateWindow) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  ResetIDs(browser()->GetTabStripModel(), 0);

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip, base::BindOnce(&DragAllToSeparateWindowStep2,
                                             this, tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(IsDragSessionActive(tab_strip2));
  ASSERT_TRUE(WaitForAttach(tab_strip2, 2));
  ASSERT_TRUE(TabDragController::IsActive());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Drag to the trailing end of the tabstrip to ensure we're in a consistent
  // spot within the strip.
  StopAnimating(tab_strip2);
  ASSERT_TRUE(DragInputToCenter(tab_strip2->tab_at(1)));

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("100 0", IDString(browser2->GetTabStripModel()));

  EXPECT_FALSE(GetIsDragged(browser2));

  // Remaining browser window should not be maximized
  EXPECT_FALSE(browser2->GetWindow()->IsMaximized());
}

#if !BUILDFLAG(IS_MAC)
namespace {

// Invoked from the nested run loop.
void CancelOnNewTabWhenDraggingStep2(DetachToBrowserTabDragControllerTest* test,
                                     BrowserWindowInterface* new_browser,
                                     base::OnceClosure quit_closure,
                                     content::WebContents** contents_out) {
  if (!new_browser) {
    new_browser = ui_test_utils::GetBrowserNotInSet({test->browser()});
  }
  CHECK(new_browser);
  EXPECT_TRUE(TabDragController::IsActive());
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Finds the new browser opened by the test, and waits until it becomes
  // the last active one.
  CHECK(new_browser);
  ui_test_utils::WaitForBrowserSetLastActive(new_browser);

  *contents_out = chrome::AddAndReturnTabAt(
      GetLastActiveBrowserWindowInterfaceWithAnyProfile(),
      GURL(url::kAboutBlankURL), 0, false);
  std::move(quit_closure).Run();
}

}  // namespace

// Adds another tab, detaches into separate window, adds another tab and
// verifies the run loop ends.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       CancelOnNewTabWhenDragging) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Skip this test for fallback tab dragging, see the note in
  // TabDragController::TabWasAdded() for more context.
  views::Widget* widget = tab_strip->GetDragContext()->GetWidget();
  if (!widget->IsMoveLoopSupported()) {
    GTEST_SKIP() << "Not relevant for fallback tab dragging";
  }

  AddTabsAndResetBrowser(browser(), 1);

  // Move to the first tab and drag it enough so that it detaches.
  const Tab* tab = tab_strip->tab_at(0);
  ASSERT_TRUE(PressInputAtCenter(tab));

  base::RunLoop run_loop;
  auto quit_closure = run_loop.QuitClosure();
  WebContents* web_contents = nullptr;
  // Add another tab. This should trigger exiting the nested loop. Add at the
  // beginning to exercise past crash when model/tabstrip got out of sync.
  // crbug.com/40081794
  AsyncBrowserWaiter waiter(
      base::BindLambdaForTesting([&](BrowserWindowInterface* new_browser) {
        CancelOnNewTabWhenDraggingStep2(this, new_browser,
                                        std::move(quit_closure), &web_contents);
      }));

  ASSERT_TRUE(
      DragInputToCenterAsync(tab, gfx::Vector2d(0, GetDetachY(tab_strip))));

  run_loop.Run();
  ASSERT_TRUE(!!web_contents);
  content::WaitForLoadStop(web_contents);

  // Should be two windows and not dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [](BrowserWindowInterface* browser) {
        EXPECT_FALSE(GetIsDragged(browser));
        // Should not be maximized
        EXPECT_FALSE(browser->GetWindow()->IsMaximized());
        return true;
      });
}
#endif  // !BUILDFLAG(IS_MAC)

namespace {

BrowserView* GetAttachedBrowserView() {
  BrowserView* result = nullptr;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&result](BrowserWindowInterface* browser) {
        BrowserView* const browser_view =
            BrowserView::GetBrowserViewForBrowser(browser);
        if (TabDragController::IsAttachedTo(
                browser_view->tab_strip_view()->GetDragContext())) {
          result = browser_view;
        }
        return !result;
      });
  return result;
}

void DragWindowAndVerifyOffset(DetachToBrowserTabDragControllerTest* test,
                               TabStrip* tab_strip,
                               int tab_index) {
  test::QuitDraggingObserver observer(tab_strip);
  // Move to the tab and drag it enough so that it detaches.
  const gfx::Point tab_center =
      GetCenterInScreenCoordinates(tab_strip->tab_at(tab_index));
  // The expected offset; the horizontal position should be relative to the
  // pressed tab. The vertical position should be relative to the window itself
  // since the top margin can be different between the existing browser and
  // the dragged one.
  const gfx::Vector2d press_offset(
      tab_center.x() - tab_strip->tab_at(tab_index)->GetBoundsInScreen().x(),
      tab_center.y() - tab_strip->GetWidget()->GetWindowBoundsInScreen().y());
  const gfx::Point initial_move =
      tab_center + gfx::Vector2d(0, GetDetachY(tab_strip));
  const gfx::Point second_move = initial_move + gfx::Vector2d(20, 20);
  const gfx::NativeWindow window_hint = test->GetWindowHint(tab_strip);
  ASSERT_TRUE(test->PressInput(tab_center, window_hint));
  ASSERT_TRUE(test->DragInputToNotifyWhenDone(
      initial_move, base::BindLambdaForTesting([&]() {
        // Moves slightly to cause the actual dragging effect on the system and
        // makes sure the window is positioned correctly.
        ASSERT_TRUE(test->DragInputToNotifyWhenDone(
            second_move, base::BindLambdaForTesting([&]() {
              BrowserView* attached_browser_view = GetAttachedBrowserView();
              TabStripRegionView* attached =
                  attached_browser_view->tab_strip_view();
              // Same computation for drag offset. This operation drags a single
              // tab, so the target tab index should be always 0.
              gfx::Vector2d drag_offset(
                  second_move.x() -
                      attached
                          ->GetTabAnchorView(attached_browser_view->browser()
                                                 ->GetTabStripModel()
                                                 ->GetTabAtIndex(0)
                                                 ->GetHandle())
                          ->GetBoundsInScreen()
                          .x(),
                  second_move.y() -
                      attached->GetWidget()->GetWindowBoundsInScreen().y());
              EXPECT_EQ(press_offset, drag_offset);
              ASSERT_TRUE(test->ReleaseInput(0, /*async=*/true));
            }),
            window_hint));
      }),
      window_hint));
  observer.Wait();
}

}  // namespace

#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_MAC) || BUILDFLAG(IS_WIN)
// TODO(mukai): enable this test on Linux.
// TODO(crbug.com/41468034): flaky on Mac, Linux and Windows.
#define MAYBE_OffsetForDraggingTab DISABLED_OffsetForDraggingTab
#else
#define MAYBE_OffsetForDraggingTab OffsetForDraggingTab
#endif

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_OffsetForDraggingTab) {
  DragWindowAndVerifyOffset(this, GetTabStripForBrowser(browser()), 0);
  ASSERT_FALSE(TabDragController::IsActive());
}

// TODO(crbug.com/41457552): fix flakiness and re-enable this test on mac/linux.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DISABLED_OffsetForDraggingDetachedTab) {
  AddTabsAndResetBrowser(browser(), 1);

  DragWindowAndVerifyOffset(this, GetTabStripForBrowser(browser()), 1);
  ASSERT_FALSE(TabDragController::IsActive());
}

class DetachToBrowserTabDragControllerTestWithFocusMode
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachToBrowserTabDragControllerTestWithFocusMode() {
    focus_mode_feature_.InitAndEnableFeature(features::kTabGroupsFocusing);
  }

 private:
  base::test::ScopedFeatureList focus_mode_feature_;
};

// In a focused tab group, dragging a tab to the right past the furthest tab in
// the group should not cause the tab to drop outside the focused group.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithFocusMode,
                       DragTabInFocusedGroupToRightDoesNotLeaveGroup) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  TabGroupModel* group_model = model->group_model();

  AddTabsAndResetBrowser(browser(), 2);
  tab_groups::TabGroupId group = model->AddToNewGroup({0, 1, 2});
  EnsureFocusToTabStrip(tab_strip);
  model->SetFocusedGroup(group);
  StopAnimating(tab_strip);

  ASSERT_EQ(3, model->count());
  ASSERT_EQ(3u, group_model->GetTabGroup(group)->ListTabs().length());
  EXPECT_EQ(group, model->GetFocusedGroup());

  // Drag tab 0 to the right past the furthest tab in the tab group (tab 2).
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(2), gfx::Vector2d(50, 0)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // All tabs should still remain in the focused group, and tab 0 moved to
  // index 2.
  EXPECT_EQ("1 2 0", IDString(model));
  EXPECT_EQ(group, model->GetFocusedGroup());
  EXPECT_EQ(3u, group_model->GetTabGroup(group)->ListTabs().length());
  EXPECT_EQ(group, model->GetTabGroupForTab(0));
  EXPECT_EQ(group, model->GetTabGroupForTab(1));
  EXPECT_EQ(group, model->GetTabGroupForTab(2));
}

// Creates two browsers, focuses a group in the second browser, then drags a
// group from the first browser into the second browser. The second browser
// should lose its focused state.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithFocusMode,
                       DragGroupHeaderIntoWindowWithFocusedGroupUnfocuses) {
  ASSERT_TRUE(browser()->GetTabStripModel()->SupportsTabGroups());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  TabStripModel* model = browser()->GetTabStripModel();
  AddTabsAndResetBrowser(browser(), 1);
  tab_groups::TabGroupId group1 = model->AddToNewGroup({0, 1});
  StopAnimating(tab_strip);

  // Create another browser with a tab group.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  TabStripModel* model2 = browser2->GetTabStripModel();
  AddTabsAndResetBrowser(browser2, 1);
  tab_groups::TabGroupId group2 = model2->AddToNewGroup({0, 1});
  StopAnimating(tab_strip2);

  // Focus group2 in browser2.
  model2->SetFocusedGroup(group2);
  ASSERT_EQ(model2->GetFocusedGroup(), group2);

  // Drag group1 by its header from browser into browser2.
  DragToDetachGroupAndNotify(tab_strip,
                             base::BindOnce(&DragAllToSeparateWindowStep2, this,
                                            tab_strip, tab_strip2),
                             group1);

  ASSERT_TRUE(WaitForAttach(tab_strip2, 4));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip2);

  // Expect browser2 to lose the focused state so both groups are occupied
  // safely.
  EXPECT_EQ(model2->GetFocusedGroup(), std::nullopt);
  EXPECT_TRUE(model2->group_model()->ContainsTabGroup(group1));
  EXPECT_TRUE(model2->group_model()->ContainsTabGroup(group2));
}

#if BUILDFLAG(IS_CHROMEOS)
namespace {

void DragInMaximizedWindowStep2(DetachToBrowserTabDragControllerTest* test,
                                BrowserWindowInterface* browser,
                                TabStrip* tab_strip) {
  // There should be another browser.
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  BrowserWindowInterface* const new_browser =
      ui_test_utils::GetBrowserNotInSet({browser});
  EXPECT_NE(browser, new_browser);
  ui_test_utils::BrowserActivationWaiter activation_waiter(new_browser);
  activation_waiter.WaitForActivation();
  EXPECT_TRUE(new_browser->GetWindow()->IsActive());
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);

  EXPECT_TRUE(IsDragSessionActive(tab_strip2));
  EXPECT_FALSE(IsDragSessionActive(tab_strip));

  // Both windows should be visible.
  EXPECT_TRUE(tab_strip->GetWidget()->IsVisible());
  EXPECT_TRUE(tab_strip2->GetWidget()->IsVisible());

  // Stops dragging.
  EXPECT_TRUE(test->ReleaseInput());
}

}  // namespace

// Creates a browser with two tabs, maximizes it, drags the tab out.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DragInMaximizedWindow) {
  {
    auto waiter = ui_test_utils::CreateAsyncWidgetRequestWaiter(*browser());
    browser()->GetWindow()->Maximize();
    waiter.Wait();
  }
  ASSERT_TRUE(browser()->GetWindow()->IsMaximized());
  AddTabsAndResetBrowser(browser(), 1);

  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  ui_test_utils::BrowserCreatedObserver browser_created_observer;
  DragTabAndNotify(tab_strip, base::BindOnce(&DragInMaximizedWindowStep2, this,
                                             browser(), tab_strip));

  // There should now be another browser.
  BrowserWindowInterface* const new_browser = browser_created_observer.Wait();
  ASSERT_TRUE(new_browser);

  ASSERT_FALSE(TabDragController::IsActive());

  WaitForBrowserActivation(new_browser);

  EXPECT_TRUE(browser()->GetWindow()->GetNativeWindow()->IsVisible());
  EXPECT_TRUE(new_browser->GetWindow()->GetNativeWindow()->IsVisible());

  EXPECT_FALSE(GetIsDragged(browser()));
  EXPECT_FALSE(GetIsDragged(new_browser));

  // The source window should be maximized.
  EXPECT_TRUE(browser()->GetWindow()->IsMaximized());
  // The new window should be maximized.
  EXPECT_TRUE(new_browser->GetWindow()->IsMaximized());
}

// Test that tab dragging can work on a browser window with its initial show
// state is MAXIMIZED.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       NewBrowserWindowState) {
  // Create a browser window whose initial show state is MAXIMIZED.
  BrowserWindowCreateParams params(browser()->GetProfile(),
                                   /*from_user_gesture=*/false);
  params.initial_show_state = ui::mojom::WindowShowState::kMaximized;
  BrowserWindowInterface* browser = CreateBrowserWindow(std::move(params));
  AddBlankTabAndShow(browser);
  AddTabsAndResetBrowser(browser, 1);

  // Maximize the browser window.
  browser->GetWindow()->Maximize();
  EXPECT_EQ(browser->GetWindow()->GetNativeWindow()->GetProperty(
                aura::client::kShowStateKey),
            ui::mojom::WindowShowState::kMaximized);

  // Drag it far enough that the first tab detaches.
  DragTabForDetachAndNotify(
      browser,
      base::BindLambdaForTesting([&](BrowserWindowInterface* source_browser,
                                     BrowserWindowInterface* detached_browser) {
        // There should be two browser windows, including the newly created one
        // for the dragged tab.
        EXPECT_EQ(3u, GlobalBrowserCollection::GetInstance()->GetSize());

        aura::Window* const window =
            detached_browser->GetWindow()->GetNativeWindow();
        EXPECT_NE(window->GetProperty(aura::client::kShowStateKey),
                  ui::mojom::WindowShowState::kMaximized);
        EXPECT_EQ(window->GetProperty(aura::client::kShowStateKey),
                  ui::mojom::WindowShowState::kDefault);

        EXPECT_TRUE(ReleaseInput());
      }));
}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       OffsetForDraggingInMaximizedWindow) {
  AddTabsAndResetBrowser(browser(), 1);
  // Moves the browser window slightly to ensure that the browser's restored
  // bounds are different from the maximized bound's origin.
  browser()->GetWindow()->SetBounds(browser()->GetWindow()->GetBounds() +
                                    gfx::Vector2d(100, 50));
  browser()->GetWindow()->Maximize();

  DragWindowAndVerifyOffset(this, GetTabStripForBrowser(browser()), 0);
  ASSERT_FALSE(TabDragController::IsActive());
}

namespace {

// A window observer that observes the dragged window's property
// ash::kIsDraggingTabsKey.
class DraggedWindowObserver : public aura::WindowObserver {
 public:
  DraggedWindowObserver(DetachToBrowserTabDragControllerTest* test,
                        aura::Window* window,
                        const gfx::Rect& bounds,
                        const gfx::Point& end_point)
      : test_(test), end_bounds_(bounds), end_point_(end_point) {}
  DraggedWindowObserver(const DraggedWindowObserver&) = delete;
  DraggedWindowObserver& operator=(const DraggedWindowObserver&) = delete;
  ~DraggedWindowObserver() override {
    if (window_) {
      window_->RemoveObserver(this);
    }
  }

  void StartObserving(aura::Window* window) {
    DCHECK(!window_);
    window_ = window;
    window_->AddObserver(this);
  }

  // aura::WindowObserver:
  void OnWindowDestroying(aura::Window* window) override {
    DCHECK_EQ(window_, window);
    window_->RemoveObserver(this);
    window_ = nullptr;
  }

  void OnWindowPropertyChanged(aura::Window* window,
                               const void* key,
                               intptr_t old) override {
    DCHECK_EQ(window_, window);
    if (key == ash::kIsDraggingTabsKey) {
      if (!window_->GetProperty(ash::kIsDraggingTabsKey)) {
        // It should be triggered by TabDragController::ClearTabDraggingInfo()
        // from TabDragController::EndDragImpl(). Theoretically at this point
        // TabDragController should have removed itself as an observer of the
        // dragged tabstrip's widget. So changing its bounds should do nothing.

        // It's to ensure the current cursor location is within the bounds of
        // another browser's tabstrip.
        test_->MoveInputTo(end_point_);

        // Change window's bounds to simulate what might happen in ash. If
        // TabDragController is still an observer of the dragged tabstrip's
        // widget, OnWidgetBoundsChanged() will calls into ContinueDragging()
        // to attach the dragged tabstrip into another browser, which might
        // cause chrome crash.
        window_->SetBounds(end_bounds_);
      }
    }
  }

 private:
  raw_ptr<DetachToBrowserTabDragControllerTest> test_;
  // The dragged window.
  raw_ptr<aura::Window> window_ = nullptr;
  // The bounds that `window_` will change to when the drag ends.
  gfx::Rect end_bounds_;
  // The position that the mouse/touch event will move to when the drag ends.
  gfx::Point end_point_;
};

void DoNotObserveDraggedWidgetAfterDragEndsStep2(
    DetachToBrowserTabDragControllerTest* test,
    DraggedWindowObserver* observer,
    TabStrip* attached_tab_strip) {
  EXPECT_TRUE(IsDragSessionActive(attached_tab_strip));
  EXPECT_TRUE(TabDragController::IsActive());

  // Start observe the dragged window.
  observer->StartObserving(attached_tab_strip->GetWidget()->GetNativeWindow());

  EXPECT_TRUE(test->ReleaseInput());
}

}  // namespace

// Test that after the drag ends, TabDragController is no longer an observer of
// the dragged widget, so that if the bounds of the dragged widget change,
// TabDragController won't be put into dragging process again.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       DoNotObserveDraggedWidgetAfterDragEnds) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateAnotherBrowserAndResize();
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Create an window observer to observe the dragged window.
  std::unique_ptr<DraggedWindowObserver> observer(new DraggedWindowObserver(
      this, test::GetWindowForTabStrip(tab_strip),
      tab_strip2->GetWidget()->GetNativeWindow()->bounds(),
      GetCenterInScreenCoordinates(tab_strip2)));

  // Drag the tab long enough so that it moves.
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DoNotObserveDraggedWidgetAfterDragEndsStep2,
                                  this, observer.get(), tab_strip));

  // There should be still two browsers at this moment. `tab_strip` should not
  // be merged into `tab_strip2`.
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  ASSERT_FALSE(TabDragController::IsActive());
}

namespace {

// Returns true if the web contents that's associated with `browser` is using
// fast resize.
bool WebContentsIsFastResized(BrowserWindowInterface* browser) {
  BrowserView* browser_view = BrowserView::GetBrowserViewForBrowser(browser);
  ContentsWebView* contents_web_view =
      views::AsViewClass<ContentsWebView>(browser_view->GetContentsView());
  return contents_web_view->holder()->fast_resize();
}

void FastResizeDuringDraggingStep2(DetachToBrowserTabDragControllerTest* test,
                                   BrowserWindowInterface* not_attached_browser,
                                   BrowserWindowInterface* target_browser) {
  TabStrip* const target_tab_strip = GetTabStripForBrowser(target_browser);
  // There should be three browser windows, including the newly created one for
  // the dragged tab.
  EXPECT_EQ(3u, GlobalBrowserCollection::GetInstance()->GetSize());

#if !BUILDFLAG(IS_LINUX)
  // Get this new created window for the drag. It should have fast resize set.
  BrowserWindowInterface* const new_browser =
      ui_test_utils::GetBrowserNotInSet({not_attached_browser, target_browser});
  EXPECT_TRUE(WebContentsIsFastResized(new_browser));
  // The source window should also have fast resize set.
  EXPECT_TRUE(WebContentsIsFastResized(test->browser()));
#endif

  // Now drag to target_tab_strip.
  EXPECT_TRUE(test->DragInputToCenter(target_tab_strip));

  EXPECT_TRUE(test->ReleaseInput());
}

}  // namespace

// Tests that we use fast resize to resize the web contents of the dragged
// window and the source window during tab dragging process, and don't use fast
// resize after tab dragging ends.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       FastResizeDuringDragging) {
  TabStrip* const tab_strip = GetTabStripForBrowser(browser());
  AddTabsAndResetBrowser(browser(), 1);

  // Create another browser.
  BrowserWindowInterface* const browser2 = CreateAnotherBrowserAndResize();
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  EXPECT_FALSE(WebContentsIsFastResized(browser()));
  EXPECT_FALSE(WebContentsIsFastResized(browser2));

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip, base::BindOnce(&FastResizeDuringDraggingStep2,
                                             this, browser(), browser2));

  EXPECT_FALSE(WebContentsIsFastResized(browser()));
  EXPECT_FALSE(WebContentsIsFastResized(browser2));
}

class DetachToBrowserTabDragControllerTestWithTabbedSystemApp
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachToBrowserTabDragControllerTestWithTabbedSystemApp()
      : test_system_web_app_installation_(
            ash::TestSystemWebAppInstallation::SetUpTabbedMultiWindowApp()) {}

  webapps::AppId InstallMockApp() {
    test_system_web_app_installation_->WaitForAppInstall();
    return test_system_web_app_installation_->GetAppId();
  }

  BrowserWindowInterface* LaunchWebAppBrowser(webapps::AppId app_id) {
    return web_app::LaunchWebAppBrowser(browser()->GetProfile(), app_id);
  }

  const GURL& GetAppUrl() {
    return test_system_web_app_installation_->GetAppUrl();
  }

 private:
  std::unique_ptr<ash::TestSystemWebAppInstallation>
      test_system_web_app_installation_;
};

// Move tab from TYPE_APP Browser to create new TYPE_APP Browser.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithTabbedSystemApp,
                       DragAppToOwnWindow) {
  // Install and get a tabbed system app.
  webapps::AppId tabbed_app_id = InstallMockApp();
  BrowserWindowInterface* const app_browser =
      LaunchWebAppBrowser(tabbed_app_id);
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Close normal browser since other code expects only 1 browser to start.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  SetBrowser(app_browser);
  ASSERT_EQ(app_browser, browser());
  EXPECT_EQ(BrowserWindowInterface::Type::TYPE_APP, browser()->GetType());
  AddTabsAndResetBrowser(browser(), 1, GetAppUrl());
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  int tab_0_width = tab_strip->tab_at(0)->width();
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(), base::BindOnce(&DetachToBrowserTabDragControllerTest::
                                    ReleaseInputAfterWindowDetached,
                                base::Unretained(this), tab_0_width));

  // New browser should be TYPE_APP.
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ(BrowserWindowInterface::Type::TYPE_APP, new_browser->GetType());
}

// TODO (crbug.com/41494298): Test fails after migrating to ChromeRefresh2023.
// Move tab from TYPE_APP Browser to another TYPE_APP Browser.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithTabbedSystemApp,
                       DISABLED_DragAppToAppWindow) {
  // Install and get 2 browsers with tabbed system app.
  webapps::AppId tabbed_app_id = InstallMockApp();
  BrowserWindowInterface* app_browser1 = LaunchWebAppBrowser(tabbed_app_id);
  BrowserWindowInterface* app_browser2 = LaunchWebAppBrowser(tabbed_app_id);
  ASSERT_EQ(3u, GlobalBrowserCollection::GetInstance()->GetSize());
  ResetIDs(app_browser2->GetTabStripModel(), 100);

  gfx::Rect work_area = display::Screen::Get()
                            ->GetDisplayNearestWindow(
                                app_browser1->GetWindow()->GetNativeWindow())
                            .work_area();
  const gfx::Size size(work_area.width() / 3, work_area.height() / 2);
  gfx::Rect browser_rect(work_area.origin(), size);
  app_browser1->GetWindow()->SetBounds(browser_rect);
  browser_rect.set_x(browser_rect.right());
  app_browser2->GetWindow()->SetBounds(browser_rect);

  // Close normal browser since other code expects only 1 browser to start.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  SetBrowser(app_browser1);
  ASSERT_EQ(app_browser1, browser());

  AddTabsAndResetBrowser(browser(), 1, GetAppUrl());
  TabStrip* const tab_strip1 = GetTabStripForBrowser(app_browser1);

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip1, base::BindOnce(&DragToSeparateWindowStep2, this,
                                              app_browser1, app_browser2));

  // Should now be attached to tab_strip2.
  // Release mouse or touch, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());
  EXPECT_EQ("100 0", IDString(app_browser2->GetTabStripModel()));
  EXPECT_EQ("1", IDString(app_browser1->GetTabStripModel()));
}

// Subclass of DetachToBrowserTabDragControllerTest that
// creates multiple displays.
class DetachToBrowserInSeparateDisplayTabDragControllerTest
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachToBrowserInSeparateDisplayTabDragControllerTest() = default;
  DetachToBrowserInSeparateDisplayTabDragControllerTest(
      const DetachToBrowserInSeparateDisplayTabDragControllerTest&) = delete;
  DetachToBrowserInSeparateDisplayTabDragControllerTest& operator=(
      const DetachToBrowserInSeparateDisplayTabDragControllerTest&) = delete;
  virtual ~DetachToBrowserInSeparateDisplayTabDragControllerTest() = default;

  void SetUpOnMainThread() override {
    DetachToBrowserTabDragControllerTest::SetUpOnMainThread();
    // Make screens sufficiently wide to host 2 browsers side by side.
    // 1280x800 is the default resolution for the main display in tests.
    // We stick to it, as opposed to a smaller one, to avoid the browser
    // window being shrunk and maximized when calling UpdateDisplay.
    const std::string display_specs = "1280x800,1280x800";
    display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
        .UpdateDisplay(display_specs);
  }
};

namespace {

void DragSingleTabToSeparateWindowInSecondDisplayStep3(
    DetachToBrowserTabDragControllerTest* test) {
  EXPECT_TRUE(test->ReleaseInput());
}

void DragSingleTabToSeparateWindowInSecondDisplayStep2(
    DetachToBrowserTabDragControllerTest* test,
    const gfx::Point& target_point,
    gfx::NativeWindow window_hint) {
  EXPECT_TRUE(test->DragInputToNotifyWhenDone(
      target_point,
      base::BindOnce(&DragSingleTabToSeparateWindowInSecondDisplayStep3, test),
      window_hint));
}

}  // namespace

// Drags from browser to a second display and releases input.
// TODO(crbug.com/40940016): Test is flaky on multiple bots.
IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest,
                       DISABLED_DragSingleTabToSeparateWindowInSecondDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move to the first tab and drag it enough so that it detaches.
  // Then drag it to the final destination on the second screen.
  display::Screen* const screen = display::Screen::Get();
  display::Display second_display = ui_test_utils::GetSecondaryDisplay(screen);
  const gfx::Point start = GetCenterInScreenCoordinates(tab_strip->tab_at(0));
  ASSERT_FALSE(second_display.bounds().Contains(start));
  const gfx::Point target(second_display.bounds().x() + 1,
                          start.y() + GetDetachY(tab_strip));
  ASSERT_TRUE(second_display.bounds().Contains(target));

  // TODO(crbug.com/40638870): Unit tests should be able to simulate mouse input
  // without having to call `CursorManager::SetDisplay`.
  ash::Shell::Get()->cursor_manager()->SetDisplay(second_display);
  ui_test_utils::BrowserCreatedObserver browser_created_observer;
  DragTabAndNotify(
      tab_strip,
      base::BindOnce(&DragSingleTabToSeparateWindowInSecondDisplayStep2, this,
                     target, GetWindowHint(tab_strip)));

  // There should now be another browser.
  BrowserWindowInterface* const new_browser = browser_created_observer.Wait();
  ASSERT_TRUE(new_browser);

  // Should no longer be dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  WaitForBrowserActivation(new_browser);
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));

  // This other browser should be on the second screen (with mouse drag)
  // With the touch input the browser cannot be dragged from one screen
  // to another and the window stays on the first screen.
  if (input_source() == InputSource::INPUT_SOURCE_MOUSE) {
    EXPECT_EQ(ui_test_utils::GetSecondaryDisplay(screen).id(),
              screen
                  ->GetDisplayNearestWindow(
                      new_browser->GetWindow()->GetNativeWindow())
                  .id());
  }

  EXPECT_EQ("0", IDString(new_browser->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));

  // Both windows should not be maximized
  EXPECT_FALSE(browser()->GetWindow()->IsMaximized());
  EXPECT_FALSE(new_browser->GetWindow()->IsMaximized());
}

namespace {

// Invoked from the nested run loop.
void DragTabToWindowInSeparateDisplayStep2(
    DetachToBrowserTabDragControllerTest* test,
    TabStrip* not_attached_tab_strip,
    TabStrip* target_tab_strip) {
  EXPECT_FALSE(IsDragSessionActive(not_attached_tab_strip));
  EXPECT_FALSE(IsDragSessionActive(target_tab_strip));
  EXPECT_TRUE(TabDragController::IsActive());

  // Drag to target_tab_strip. This should stop the nested loop from dragging
  // the window.
  // Move it closer to the beginning of the tab so it will drop before that tab.
  EXPECT_TRUE(test->DragInputToCenterAsync(target_tab_strip->tab_at(0),
                                           gfx::Vector2d(-20, 0)));
}

}  // namespace

// Drags from browser to another browser on a second display and releases input.
// TODO(crbug.com/329747667): Test is flaky on "Linux ChromiumOS MSan Tests"
#if BUILDFLAG(IS_CHROMEOS) && defined(MEMORY_SANITIZER)
#define MAYBE_DragTabToWindowInSeparateDisplay \
  DISABLED_DragTabToWindowInSeparateDisplay
#else
#define MAYBE_DragTabToWindowInSeparateDisplay DragTabToWindowInSeparateDisplay
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest,
                       MAYBE_DragTabToWindowInSeparateDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  ResetIDs(browser2->GetTabStripModel(), 100);

  // Move the second browser to the second display.
  display::Screen* screen = display::Screen::Get();
  Display second_display = ui_test_utils::GetSecondaryDisplay(screen);
  ui_test_utils::SetAndWaitForBounds(*browser2, second_display.work_area());
  EXPECT_EQ(
      second_display.id(),
      screen->GetDisplayNearestWindow(browser2->GetWindow()->GetNativeWindow())
          .id());

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DragTabToWindowInSeparateDisplayStep2, this,
                                  tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_TRUE(TabDragController::IsActive());

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 100", IDString(browser2->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));

  // Both windows should not be maximized
  EXPECT_FALSE(browser()->GetWindow()->IsMaximized());
  EXPECT_FALSE(browser2->GetWindow()->IsMaximized());
}

// Disabling test failing on ChromeOS. crbug.com/40647142
IN_PROC_BROWSER_TEST_P(
    DetachToBrowserInSeparateDisplayTabDragControllerTest,
    DISABLED_DragBrowserWindowWhenMajorityOfBoundsInSecondDisplay) {
  // Set the browser's window bounds such that the majority of its bounds
  // resides in the second display.
  const std::pair<Display, Display> displays =
      GetDisplays(display::Screen::Get());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  {
    // Moves the browser window through dragging so that the majority of its
    // bounds are in the secondary display but it's still be in the primary
    // display. Do not use SetBounds() or related, it may move the browser
    // window to the secondary display in some configurations like Mash.
    int target_x = displays.first.bounds().right() -
                   browser()->GetWindow()->GetBounds().width() / 2 + 20;
    const gfx::Point target_point =
        GetCenterInScreenCoordinates(tab_strip->tab_at(0)) +
        gfx::Vector2d(target_x - browser()->GetWindow()->GetBounds().x(),
                      GetDetachY(tab_strip));
    DragTabAndNotify(
        tab_strip,
        base::BindOnce(&DragSingleTabToSeparateWindowInSecondDisplayStep2, this,
                       target_point, GetWindowHint(tab_strip)));
    StopAnimating(tab_strip);
  }
  EXPECT_EQ(
      displays.first.id(),
      browser()->GetWindow()->GetNativeWindow()->GetHost()->GetDisplayId());

  // Start dragging the window by the tab strip, and move it only to the edge
  // of the first display. Expect at that point mouse would warp and the window
  // will therefore reside in the second display when mouse is released. Set the
  // target destination to be 1/8th mark of the second display. This is to
  // prevent the window from snapping to the left side of the display when
  // dropped near the edge.
  const gfx::Point tab_0_center =
      GetCenterInScreenCoordinates(tab_strip->tab_at(0));
  const int offset_x =
      tab_0_center.x() - browser()->GetWindow()->GetBounds().x();
  const int detach_y = tab_0_center.y() + GetDetachY(tab_strip);
  const int first_display_warp_edge_x = displays.first.bounds().right() - 1;
  const gfx::Point second_display_target_point(
      displays.second.bounds().x() + (displays.second.bounds().width() / 8),
      detach_y);

  DragTabAndNotify(
      tab_strip, base::BindLambdaForTesting([&]() {
        // This makes another event on the warped location because the test
        // system does not create it automatically as the result of pointer
        // warp.
        const gfx::NativeWindow window_hint = GetWindowHint(tab_strip);
        ASSERT_TRUE(DragInputToNotifyWhenDone(
            gfx::Point(first_display_warp_edge_x, detach_y),
            base::BindOnce(&DragSingleTabToSeparateWindowInSecondDisplayStep2,
                           this, second_display_target_point, window_hint),
            window_hint));
      }));

  // Should no longer be dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  // There should only be a single browser.
  WaitForBrowserActivation(browser());
  ASSERT_FALSE(IsDragSessionActive(tab_strip));

  // Browser now resides in display 2.
  EXPECT_EQ(second_display_target_point.x() - offset_x,
            browser()->GetWindow()->GetBounds().x());
  EXPECT_EQ(
      displays.second.id(),
      browser()->GetWindow()->GetNativeWindow()->GetHost()->GetDisplayId());
}

// Drags from browser to another browser on a second display and releases input.
#if BUILDFLAG(IS_CHROMEOS) && defined(MEMORY_SANITIZER)
// TODO(crbug.com/333006829):
#define MAYBE_DragTabToWindowOnSecondDisplay \
  DISABLED_DragTabToWindowOnSecondDisplay
#else
#define MAYBE_DragTabToWindowOnSecondDisplay DragTabToWindowOnSecondDisplay
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest,
                       MAYBE_DragTabToWindowOnSecondDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  ResetIDs(browser2->GetTabStripModel(), 100);

  // Move both browsers to be side by side on the second display.
  display::Screen* screen = display::Screen::Get();
  Display second_display = ui_test_utils::GetSecondaryDisplay(screen);
  gfx::Rect work_area = second_display.work_area();
  work_area.set_width(work_area.width() / 2);
  ui_test_utils::SetAndWaitForBounds(*browser(), work_area);
  // It's possible the window will not fit in half the screen, in which case we
  // will position the windows as well as we can.
  work_area.set_x(browser()->GetWindow()->GetBounds().right());
  // Sanity check: second browser should still be on the second display.
  ASSERT_LT(work_area.x(), second_display.work_area().right());
  ui_test_utils::SetAndWaitForBounds(*browser2, work_area);
  EXPECT_EQ(
      second_display.id(),
      screen->GetDisplayNearestWindow(browser()->GetWindow()->GetNativeWindow())
          .id());
  EXPECT_EQ(
      second_display.id(),
      screen->GetDisplayNearestWindow(browser2->GetWindow()->GetNativeWindow())
          .id());

  // Sanity check: make sure the target position is also within in the screen
  // bounds:
  ASSERT_LT(GetCenterInScreenCoordinates(tab_strip2->tab_at(0)).x(),
            second_display.work_area().right());

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DragTabToWindowInSeparateDisplayStep2, this,
                                  tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_TRUE(TabDragController::IsActive());

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 100", IDString(browser2->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));

  // Both windows should not be maximized
  EXPECT_FALSE(browser()->GetWindow()->IsMaximized());
  EXPECT_FALSE(browser2->GetWindow()->IsMaximized());
}

// Drags from a maximized browser to another non-maximized browser on a second
// display and releases input.
#if BUILDFLAG(IS_CHROMEOS) && defined(MEMORY_SANITIZER)
// TODO(crbug.com/333006829):
#define MAYBE_DragMaxTabToNonMaxWindowInSeparateDisplay \
  DISABLED_DragMaxTabToNonMaxWindowInSeparateDisplay
#else
#define MAYBE_DragMaxTabToNonMaxWindowInSeparateDisplay \
  DragMaxTabToNonMaxWindowInSeparateDisplay
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest,
                       MAYBE_DragMaxTabToNonMaxWindowInSeparateDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  ASSERT_TRUE(ui_test_utils::MaximizeAndWaitUntilUIUpdateDone(*browser()));
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Create another browser on the second display.
  display::Screen* screen = display::Screen::Get();
  ASSERT_EQ(2, screen->GetNumDisplays());
  const std::pair<Display, Display> displays = GetDisplays(screen);
  gfx::Rect work_area = displays.second.work_area();
  work_area.Inset(gfx::Insets::TLBR(20, 20, 60, 20));
  BrowserWindowCreateParams params(browser()->GetProfile(),
                                   /*from_user_gesture=*/true);
  params.initial_show_state = ui::mojom::WindowShowState::kNormal;
  params.initial_bounds = work_area;
  BrowserWindowInterface* browser2 = CreateBrowserWindow(std::move(params));
  AddBlankTabAndShow(browser2);

  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  ResetIDs(browser2->GetTabStripModel(), 100);

  EXPECT_EQ(
      displays.second.id(),
      screen->GetDisplayNearestWindow(browser2->GetWindow()->GetNativeWindow())
          .id());
  EXPECT_EQ(
      displays.first.id(),
      screen->GetDisplayNearestWindow(browser()->GetWindow()->GetNativeWindow())
          .id());
  EXPECT_EQ(2, tab_strip->GetTabCount());
  EXPECT_EQ(1, tab_strip2->GetTabCount());

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DragTabToWindowInSeparateDisplayStep2, this,
                                  tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_TRUE(TabDragController::IsActive());

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());

  // tab should have moved
  EXPECT_EQ(1, tab_strip->GetTabCount());
  EXPECT_EQ(2, tab_strip2->GetTabCount());

  ASSERT_FALSE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 100", IDString(browser2->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));

  // Source browser should still be maximized, target should not
  EXPECT_TRUE(browser()->GetWindow()->IsMaximized());
  EXPECT_FALSE(browser2->GetWindow()->IsMaximized());
}

// Drags from a restored browser to an immersive fullscreen browser on a
// second display and releases input.
// TODO(pkasting) https://crbug.com/41429340 Hangs.
IN_PROC_BROWSER_TEST_P(DetachToBrowserInSeparateDisplayTabDragControllerTest,
                       DISABLED_DragTabToImmersiveBrowserOnSeparateDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Create another browser.
  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  ResetIDs(browser2->GetTabStripModel(), 100);

  // Move the second browser to the second display.
  display::Screen* screen = display::Screen::Get();
  const std::pair<Display, Display> displays = GetDisplays(screen);
  ui_test_utils::SetAndWaitForBounds(*browser2, displays.second.work_area());
  EXPECT_EQ(
      displays.second.id(),
      screen->GetDisplayNearestWindow(browser2->GetWindow()->GetNativeWindow())
          .id());

  // Put the second browser into immersive fullscreen.
  auto* const immersive_controller2 = ImmersiveModeController::From(browser2);
  chromeos::ImmersiveFullscreenControllerTestApi(
      static_cast<ImmersiveModeControllerChromeos*>(immersive_controller2)
          ->controller())
      .SetupForTest();
  chrome::ToggleFullscreenMode(browser2);
  // For MD, the browser's top chrome is completely offscreen, with tabstrip
  // visible.
  ASSERT_TRUE(immersive_controller2->IsEnabled());
  ASSERT_FALSE(immersive_controller2->IsRevealed());
  ASSERT_TRUE(tab_strip2->GetVisible());

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough that it attaches to browser2.
  DragTabAndNotify(tab_strip,
                   base::BindOnce(&DragTabToWindowInSeparateDisplayStep2, this,
                                  tab_strip, tab_strip2));

  // Should now be attached to tab_strip2.
  ASSERT_TRUE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_TRUE(TabDragController::IsActive());

  // browser2's top chrome should be revealed and the tab strip should be
  // at normal height while user is dragging tabs_strip2's tabs.
  ASSERT_TRUE(immersive_controller2->IsRevealed());
  ASSERT_TRUE(tab_strip2->GetVisible());

  // Release the mouse, stopping the drag session.
  ASSERT_TRUE(ReleaseInput());
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 100", IDString(browser2->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));

  // Move the mouse off of browser2's top chrome.
  ASSERT_TRUE(
      ui_test_utils::SendMouseMoveSync(displays.first.bounds().CenterPoint()));

  // The first browser window should not be in immersive fullscreen.
  // browser2 should still be in immersive fullscreen, but the top chrome should
  // no longer be revealed.
  EXPECT_FALSE(ImmersiveModeController::From(browser())->IsEnabled());

  EXPECT_TRUE(immersive_controller2->IsEnabled());
  EXPECT_FALSE(immersive_controller2->IsRevealed());
  EXPECT_TRUE(tab_strip2->GetVisible());
}

// Subclass of DetachToBrowserTabDragControllerTest that
// creates multiple displays with different device scale factors.
class DifferentDeviceScaleFactorDisplayTabDragControllerTest
    : public DetachToBrowserTabDragControllerTest {
 public:
  DifferentDeviceScaleFactorDisplayTabDragControllerTest() = default;
  DifferentDeviceScaleFactorDisplayTabDragControllerTest(
      const DifferentDeviceScaleFactorDisplayTabDragControllerTest&) = delete;
  DifferentDeviceScaleFactorDisplayTabDragControllerTest& operator=(
      const DifferentDeviceScaleFactorDisplayTabDragControllerTest&) = delete;
  virtual ~DifferentDeviceScaleFactorDisplayTabDragControllerTest() = default;

  void SetUpOnMainThread() override {
    DetachToBrowserTabDragControllerTest::SetUpOnMainThread();
    display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
        .UpdateDisplay("1280x800,1280x800*2");
  }

  float GetCursorDeviceScaleFactor() const {
    auto* cursor_client = aura::client::GetCursorClient(
        browser()->GetWindow()->GetNativeWindow()->GetRootWindow());
    const auto& cursor_shape_client = aura::client::GetCursorShapeClient();
    return cursor_shape_client.GetCursorData(cursor_client->GetCursor())
        ->scale_factor;
  }
};

namespace {

// The points where a tab is dragged in CursorDeviceScaleFactorStep.
constexpr auto kDragPoints = std::to_array<gfx::Point>({
    {300, 200},
    {399, 200},
    {500, 200},
    {400, 200},
    {300, 200},
});

// The expected device scale factors after the cursor is moved to the
// corresponding kDragPoints in CursorDeviceScaleFactorStep.
constexpr std::array kDeviceScaleFactorExpectations = {
    1.0f, 1.0f, 2.0f, 2.0f, 1.0f,
};

static_assert(
    kDragPoints.size() == kDeviceScaleFactorExpectations.size(),
    "kDragPoints and kDeviceScaleFactorExpectations must have the same "
    "number of elements");

// Drags tab to `kDragPoints[index]`, then calls the next step function.
void CursorDeviceScaleFactorStep(
    DifferentDeviceScaleFactorDisplayTabDragControllerTest* test,
    TabStrip* not_attached_tab_strip,
    size_t index) {
  SCOPED_TRACE(index);
  ASSERT_FALSE(IsDragSessionActive(not_attached_tab_strip));
  ASSERT_TRUE(TabDragController::IsActive());

  if (index > 0) {
    EXPECT_EQ(kDragPoints[index - 1],
              aura::Env::GetInstance()->last_mouse_location());
    EXPECT_EQ(kDeviceScaleFactorExpectations[index - 1],
              test->GetCursorDeviceScaleFactor());
  }

  if (index < std::size(kDragPoints)) {
    ASSERT_TRUE(test->DragInputToNotifyWhenDone(
        kDragPoints[index],
        base::BindOnce(&CursorDeviceScaleFactorStep, test,
                       not_attached_tab_strip, index + 1),
        // The window hint isn't used on Ash.
        gfx::NativeWindow()));
  } else {
    // Finishes a series of CursorDeviceScaleFactorStep calls and ends drag.
    ASSERT_TRUE(
        ui_test_utils::SendMouseEventsSync(ui_controls::LEFT, ui_controls::UP));
  }
}

}  // namespace

// Verifies cursor's device scale factor is updated when a tab is moved across
// displays with different device scale factors (http://crbug.com/40951059).
// TODO(pkasting): In interactive_ui_tests, scale factor never changes to 2.
// https://crbug.com/41433816
// TODO(pkasting): In non_single_process_mash_interactive_ui_tests, pointer is
// warped during the drag (which results in changing to scale factor 2 early),
// and scale factor doesn't change back to 1 at the end.
// https://crbug.com/40608081
IN_PROC_BROWSER_TEST_P(DifferentDeviceScaleFactorDisplayTabDragControllerTest,
                       DISABLED_CursorDeviceScaleFactor) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Move the second browser to the second display.
  ASSERT_EQ(2, display::Screen::Get()->GetNumDisplays());

  // Move to the first tab and drag it enough so that it detaches.
  DragTabAndNotify(tab_strip, base::BindOnce(&CursorDeviceScaleFactorStep, this,
                                             tab_strip, 0));
}

class DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest() = default;
  DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest(
      const DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest&) =
      delete;
  DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest& operator=(
      const DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest&) =
      delete;

  void SetUpOnMainThread() override {
    DetachToBrowserTabDragControllerTest::SetUpOnMainThread();
    display::test::DisplayManagerTestApi(ash::Shell::Get()->display_manager())
        .UpdateDisplay("1280x800,1280x800");
  }
};

namespace {

// Invoked from the nested run loop.
void CancelDragTabToWindowInSeparateDisplayStep3(
    DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest* test,
    TabStrip* tab_strip) {
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_TRUE(TabDragController::IsActive());
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Switching display mode should cancel the drag operation.
  ash::ShellTestApi().AddRemoveDisplay();
}

// Invoked from the nested run loop.
void CancelDragTabToWindowInSeparateDisplayStep2(
    DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest* test,
    BrowserWindowInterface* browser,
    Display current_display,
    gfx::Point final_destination) {
  TabStrip* const tab_strip = GetTabStripForBrowser(browser);

  EXPECT_FALSE(IsDragSessionActive(tab_strip));
  EXPECT_TRUE(TabDragController::IsActive());
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  BrowserWindowInterface* const new_browser =
      ui_test_utils::GetBrowserNotInSet({browser});
  EXPECT_EQ(
      current_display.id(),
      display::Screen::Get()
          ->GetDisplayNearestWindow(new_browser->GetWindow()->GetNativeWindow())
          .id());

  EXPECT_TRUE(test->DragInputToNotifyWhenDone(
      final_destination,
      base::BindOnce(&CancelDragTabToWindowInSeparateDisplayStep3, test,
                     tab_strip),
      // The window hint isn't used on Ash.
      gfx::NativeWindow()));
}

}  // namespace

// Drags from browser to a second display and releases input.
IN_PROC_BROWSER_TEST_P(
    DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest,
    CancelDragTabToWindowIn2ndDisplay) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  EXPECT_EQ("0 1", IDString(browser()->GetTabStripModel()));

  // Move the second browser to the second display.
  const std::pair<Display, Display> displays =
      GetDisplays(display::Screen::Get());
  gfx::Point final_destination = displays.second.work_area().CenterPoint();

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough to move to another display.
  DragTabAndNotify(
      tab_strip,
      base::BindOnce(&CancelDragTabToWindowInSeparateDisplayStep2, this,
                     browser(), displays.first, final_destination));

  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 1", IDString(browser()->GetTabStripModel()));

  // Release the mouse
  ASSERT_TRUE(
      ui_test_utils::SendMouseEventsSync(ui_controls::LEFT, ui_controls::UP));
}

// Drags from browser from a second display to primary and releases input.
IN_PROC_BROWSER_TEST_P(
    DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest,
    CancelDragTabToWindowIn1stDisplay) {
  display::Screen* screen = display::Screen::Get();
  const std::pair<Display, Display> displays = GetDisplays(screen);

  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  EXPECT_EQ("0 1", IDString(browser()->GetTabStripModel()));
  EXPECT_EQ(
      displays.first.id(),
      screen->GetDisplayNearestWindow(browser()->GetWindow()->GetNativeWindow())
          .id());

  browser()->GetWindow()->SetBounds(displays.second.work_area());
  EXPECT_EQ(
      displays.second.id(),
      screen->GetDisplayNearestWindow(browser()->GetWindow()->GetNativeWindow())
          .id());

  // Move the second browser to the display.
  gfx::Point final_destination = displays.first.work_area().CenterPoint();

  // Move to the first tab and drag it enough so that it detaches, but not
  // enough to move to another display.
  DragTabAndNotify(
      tab_strip,
      base::BindOnce(&CancelDragTabToWindowInSeparateDisplayStep2, this,
                     browser(), displays.second, final_destination));

  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ("0 1", IDString(browser()->GetTabStripModel()));

  // Release the mouse
  ASSERT_TRUE(
      ui_test_utils::SendMouseEventsSync(ui_controls::LEFT, ui_controls::UP));
}

// Subclass of DetachToBrowserTabDragControllerTest that runs tests only with
// touch input.
class DetachToBrowserTabDragControllerTestTouch
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachToBrowserTabDragControllerTestTouch() = default;
  DetachToBrowserTabDragControllerTestTouch(
      const DetachToBrowserTabDragControllerTestTouch&) = delete;
  DetachToBrowserTabDragControllerTestTouch& operator=(
      const DetachToBrowserTabDragControllerTestTouch&) = delete;
  virtual ~DetachToBrowserTabDragControllerTestTouch() = default;

  void TearDown() override {
    ui::SetEventTickClockForTesting(nullptr);
    clock_.reset();
  }

 protected:
  std::unique_ptr<base::SimpleTestTickClock> clock_;
};

#if BUILDFLAG(IS_CHROMEOS)
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestTouch,
                       SourceWindowClosedDuringDrag) {
  AddTabsAndResetBrowser(browser(), 1);
  DragTabForDetachAndNotify(
      browser(),
      base::BindLambdaForTesting([&](BrowserWindowInterface* source_browser,
                                     BrowserWindowInterface* detached_browser) {
        // This callback runs inside the nested move loop.

        chrome::CloseWindow(browser());

        // Post a task (to run after the above window closing has completed)
        // that does something that depends on the drag window's source window
        // property.
        base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
            FROM_HERE, base::BindLambdaForTesting([this]() {
              // Enter tablet mode to trigger TabletModeWindowState creation,
              // which reads the source window property.
              ash::TabletMode::Waiter waiter(true);
              ash::TabletMode::Get()->SetEnabledForTest(true);
              waiter.Wait();
              ash::TabletMode::Get()->SetEnabledForTest(false);
              EXPECT_TRUE(
                  static_cast<DetachToBrowserTabDragControllerTest*>(this)
                      ->ReleaseInput(0));
            }));
      }),
      1);  // Drag tab at index 1.
  EXPECT_FALSE(TabDragController::IsActive());
}
#endif

// Detaches a tab and while detached presses a second finger.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestTouch,
                       PressSecondFingerWhileDetached) {
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  EXPECT_EQ("0 1", IDString(browser()->GetTabStripModel()));

  // Move to the first tab and drag it enough so that it detaches. Drag it
  // slightly more horizontally so that it does not generate a swipe down
  // gesture that minimizes the detached browser window.
  const int touch_move_delta = GetDetachY(tab_strip);
  const gfx::Point target = GetCenterInScreenCoordinates(tab_strip->tab_at(0)) +
                            gfx::Vector2d(0, 2 * touch_move_delta);

  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(),
      base::BindLambdaForTesting([&](BrowserWindowInterface* source_browser,
                                     BrowserWindowInterface* detached_browser) {
        EXPECT_TRUE(TabDragController::IsActive());
        EXPECT_TRUE(detached_browser->GetWindow()->IsActive());

        // The window hint isn't used on Ash.
        gfx::NativeWindow window_hint = gfx::NativeWindow();
        // Continue dragging after adding a second finger.
        EXPECT_TRUE(PressInput(gfx::Point(), window_hint, 1));
        EXPECT_TRUE(DragInputToNotifyWhenDone(
            target, base::BindLambdaForTesting([&]() {
              EXPECT_TRUE(TabDragController::IsActive());
              EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
              EXPECT_TRUE(ui_test_utils::GetBrowserNotInSet({browser()})
                              ->GetWindow()
                              ->IsActive());

              EXPECT_TRUE(ReleaseInput());
              EXPECT_TRUE(ReleaseInput(1));
            }),
            window_hint));
      }),
      0, touch_move_delta + 5);
  ASSERT_TRUE(new_browser);

  // Should no longer be dragging.
  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());

  WaitForBrowserActivation(new_browser);
  TabStrip* tab_strip2 = GetTabStripForBrowser(new_browser);
  ASSERT_FALSE(IsDragSessionActive(tab_strip2));

  EXPECT_EQ("0", IDString(new_browser->GetTabStripModel()));
  EXPECT_EQ("1", IDString(browser()->GetTabStripModel()));
}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestTouch,
                       LeftSnapShouldntCauseMergeAtEnd) {
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  AddTabsAndResetBrowser(browser(), 1);

  // Set the last mouse location at the center of tab 0. This shouldn't affect
  // the touch behavior below. See https://crbug.com/40606256#comment2 for the
  // details of how this can affect the result.
  gfx::Point tab_0_center(GetCenterInScreenCoordinates(tab_strip->tab_at(0)));
  base::RunLoop run_loop;
  ui_controls::SendMouseMoveNotifyWhenDone(tab_0_center.x(), tab_0_center.y(),
                                           run_loop.QuitClosure());
  run_loop.Run();

  // Drag the tab 1 to left-snapping.
  DragTabAndNotify(tab_strip, base::BindLambdaForTesting([&]() {
                     const gfx::Rect display_bounds =
                         display::Screen::Get()->GetPrimaryDisplay().bounds();
                     const gfx::Point target(display_bounds.x(),
                                             display_bounds.CenterPoint().y());
                     ASSERT_TRUE(DragInputToNotifyWhenDone(
                         target, base::BindLambdaForTesting([&]() {
                           ASSERT_TRUE(ReleaseInput());
                         }),
                         // The window hint isn't used on Ash.
                         gfx::NativeWindow()));
                   }),
                   1);

  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestTouch,
                       FlingDownAtEndOfDrag) {
  // Reduce the minimum fling velocity for this specific test case to cause the
  // fling-down gesture in the middle of tab-dragging. This should end up with
  // minimizing the window. See https://crbug.com/40601420 for the details.
  SetMinFlingVelocity(1);

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  test::QuitDraggingObserver observer(tab_strip);
  const gfx::Point tab_0_center =
      GetCenterInScreenCoordinates(tab_strip->tab_at(0));
  const gfx::Vector2d detach(0, GetDetachY(tab_strip));
  const gfx::NativeWindow window_hint = GetWindowHint(tab_strip);
  clock_ = std::make_unique<base::SimpleTestTickClock>();
  clock_->SetNowTicks(base::TimeTicks::Now());
  ui::SetEventTickClockForTesting(clock_.get());
  ASSERT_TRUE(PressInput(tab_0_center, window_hint));
  clock_->Advance(base::Milliseconds(5));
  ASSERT_TRUE(DragInputToNotifyWhenDone(
      tab_0_center + detach, base::BindLambdaForTesting([&]() {
        // Drag down again; this should cause a fling-down event.
        clock_->Advance(base::Milliseconds(5));
        ASSERT_TRUE(DragInputToNotifyWhenDone(
            tab_0_center + detach + detach, base::BindLambdaForTesting([&]() {
              clock_->Advance(base::Milliseconds(5));
              ASSERT_TRUE(ReleaseInput());
            }),
            window_hint));
      }),
      window_hint));
  observer.Wait();

  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_TRUE(browser()->GetWindow()->IsMinimized());
  EXPECT_FALSE(browser()->GetWindow()->IsVisible());
}

// TODO(http://crbug.com/343503164) This test seems to be attempting to induce a
// fling gesture event, but it currently fails to do so. If a fling gesture
// event was produced, the detached window should end up minimized.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestTouch,
                       DISABLED_FlingOnStartingDrag) {
  SetMinFlingVelocity(1);
  AddTabsAndResetBrowser(browser(), 1);
  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  const gfx::Point tab_0_center =
      GetCenterInScreenCoordinates(tab_strip->tab_at(0));
  const gfx::Vector2d detach(0, GetDetachY(tab_strip));
  const gfx::NativeWindow window_hint = GetWindowHint(tab_strip);

  // Sends events to the server without waiting for its reply, which will cause
  // extra touch events before PerformWindowMove starts handling events.
  ui_test_utils::BrowserCreatedObserver browser_created_observer;
  test::QuitDraggingObserver observer(tab_strip);
  clock_ = std::make_unique<base::SimpleTestTickClock>();
  clock_->SetNowTicks(base::TimeTicks::Now());
  ui::SetEventTickClockForTesting(clock_.get());
  ASSERT_TRUE(PressInput(tab_0_center, window_hint));
  clock_->Advance(base::Milliseconds(5));
  ASSERT_TRUE(DragInputToAsync(tab_0_center + detach, window_hint));
  clock_->Advance(base::Milliseconds(5));
  ASSERT_TRUE(DragInputToAsync(tab_0_center + detach + detach, window_hint));
  clock_->Advance(base::Milliseconds(2));
  ASSERT_TRUE(ReleaseInput());
  observer.Wait();
  BrowserWindowInterface* const browser2 = browser_created_observer.Wait();

  ASSERT_FALSE(IsDragSessionActive(tab_strip));
  ASSERT_FALSE(TabDragController::IsActive());
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_TRUE(browser2->GetWindow()->IsMinimized());
  EXPECT_FALSE(browser2->GetWindow()->IsVisible());
}

#endif  // BUILDFLAG(IS_CHROMEOS)

namespace {

class SelectTabDuringDragObserver : public TabStripModelObserver {
 public:
  SelectTabDuringDragObserver() = default;
  ~SelectTabDuringDragObserver() override = default;
  void OnTabStripModelChanged(
      TabStripModel* tab_strip_model,
      const TabStripModelChange& change,
      const TabStripSelectionChange& selection) override {
    if (change.type() != TabStripModelChange::kMoved) {
      return;
    }
    const TabStripModelChange::Move* move = change.GetMove();
    int index_to_select = move->to_index == 0 ? 1 : 0;
    tab_strip_model->SelectTabAt(index_to_select);
  }
};

}  // namespace

// Bug fix for crbug.com/40055468. Don't change tab selection while dragging.
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       SelectTabDuringDrag) {
  TabStripModel* model = browser()->GetTabStripModel();
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  SelectTabDuringDragObserver observer;
  model->AddObserver(&observer);

  AddTabsAndResetBrowser(browser(), 1);
  ASSERT_EQ(2, model->count());

  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1)));
  {
    gfx::Rect tab_bounds = tab_strip->tab_at(1)->GetLocalBounds();
    views::View::ConvertRectToScreen(tab_strip->tab_at(1), &tab_bounds);
    ASSERT_TRUE(
        DragInputTo(tab_bounds.right_center(), GetWindowHint(tab_strip)));
  }
  ASSERT_TRUE(ReleaseInput());
}

// Regression test for http://crbug.com/505371980.
// Verifies that the correct tab is detached even if the selection changes
// mid-drag.
#if BUILDFLAG(IS_WIN)
#define MAYBE_SelectTabDuringDragAndDetach SelectTabDuringDragAndDetach
#else
#define MAYBE_SelectTabDuringDragAndDetach DISABLED_SelectTabDuringDragAndDetach
#endif
IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTest,
                       MAYBE_SelectTabDuringDragAndDetach) {
  TabStripModel* model = browser()->GetTabStripModel();
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  AddTabsAndResetBrowser(browser(), 1);
  ASSERT_EQ(2, model->count());
  // The second tab (index 1) is active.
  ASSERT_EQ(1, model->active_index());

  // Save the contents of the tabs to verify later.
  content::WebContents* tab0_contents = model->GetWebContentsAt(0);
  content::WebContents* tab1_contents = model->GetWebContentsAt(1);

  // Use QuitDraggingObserver to wait for the drag to end safely.
  test::QuitDraggingObserver observer(tab_strip);

  // Start dragging tab 0.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));

  // Drag slightly to start the drag, then change selection and detach.
  // DragInputToCenterNotifyWhenDone is more robust as it handles the potential
  // transition into a nested move loop.
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      tab_strip->tab_at(0), base::BindLambdaForTesting([&]() {
        // 1. Change selection mid-drag.
        ui::ListSelectionModel new_selection;
        new_selection.SetSelectedIndex(1);
        model->SetSelectionFromModel(std::move(new_selection));

        // 2. Trigger detach with a larger offset to ensure it crosses the
        // threshold on all bots.
        DragInputToCenterAsync(tab_strip->tab_at(0),
                               gfx::Vector2d(0, GetDetachY(tab_strip) + 20));

        // 3. Use async release to avoid hanging in the move loop.
        ReleaseInput(0, true);
      }),
      gfx::Vector2d(0, 5)));

  // Wait for the drag to end.
  observer.Wait();

  // Verify that a new browser was created and it contains tab 0.
  ASSERT_EQ(2u, GetAllBrowserWindowInterfaces().size());
  BrowserWindowInterface* new_browser =
      ui_test_utils::GetBrowserNotInSet({browser()});
  ASSERT_TRUE(new_browser);

  EXPECT_EQ(1, new_browser->GetTabStripModel()->count());
  EXPECT_EQ(tab0_contents,
            new_browser->GetTabStripModel()->GetWebContentsAt(0));

  // Verify that the original browser still contains tab 1.
  EXPECT_EQ(1, model->count());
  EXPECT_EQ(tab1_contents, model->GetWebContentsAt(0));
}

#if BUILDFLAG(IS_CHROMEOS)
// Runs tests with a tabbed system web app that is locked for OnTask. This is
// not related to normal web browsers.
using DetachToBrowserTabDragControllerTestWithOnTaskLocked =
    DetachToBrowserTabDragControllerTestWithTabbedSystemApp;

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithOnTaskLocked,
                       DoesNotMoveTabOnDrag) {
  // Install and launch mock app that can be locked for OnTask.
  const webapps::AppId tabbed_app_id = InstallMockApp();
  BrowserWindowInterface* const app_browser =
      LaunchWebAppBrowser(tabbed_app_id);
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Close normal browser.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  SetBrowser(app_browser);
  ASSERT_EQ(app_browser, browser());
  EXPECT_EQ(BrowserWindowInterface::Type::TYPE_APP, browser()->GetType());

  // Lock the app for OnTask and set up app for testing drag behavior.
  ash::boca::OnTaskLockedController::From(browser())->set_locked_for_on_task(
      true);
  AddTabsAndResetBrowser(browser(), /*additional_tabs=*/3, GetAppUrl());
  TabStripModel* const tab_strip_model = browser()->GetTabStripModel();
  ASSERT_EQ("0 1 2 3", IDString(tab_strip_model));

  // Attempt to drag tab in the second index to the tab in the third index to
  // switch tab positioning.
  TabStrip* const tab_strip = GetTabStripForBrowser(browser());
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // Verify tab is not detached and its position remains unchanged.
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ("0 1 2 3", IDString(tab_strip_model));
}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithOnTaskLocked,
                       TabDoesNotDetachOnDrag) {
  // Install and launch mock app that can be locked for OnTask.
  const webapps::AppId tabbed_app_id = InstallMockApp();
  BrowserWindowInterface* const app_browser =
      LaunchWebAppBrowser(tabbed_app_id);
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Close normal browser.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  SetBrowser(app_browser);
  ASSERT_EQ(app_browser, browser());
  EXPECT_EQ(BrowserWindowInterface::Type::TYPE_APP, browser()->GetType());

  // Lock the app for OnTask and set up app for testing drag behavior.
  ash::boca::OnTaskLockedController::From(browser())->set_locked_for_on_task(
      true);
  AddTabsAndResetBrowser(browser(), /*additional_tabs=*/3, GetAppUrl());

  // Drag tab away from tab strip and verify it is not detached.
  TabStrip* const tab_strip = GetTabStripForBrowser(browser());
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(1)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(1),
                                gfx::Vector2d(0, GetDetachY(tab_strip) + 1)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
}

IN_PROC_BROWSER_TEST_P(DetachToBrowserTabDragControllerTestWithOnTaskLocked,
                       WindowDoesNotMoveOnTabDrag) {
  // Install and launch mock app that can be locked for OnTask.
  const webapps::AppId tabbed_app_id = InstallMockApp();
  BrowserWindowInterface* const app_browser =
      LaunchWebAppBrowser(tabbed_app_id);
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Close normal browser.
  CloseBrowserSynchronously(browser());
  ASSERT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  SetBrowser(app_browser);
  ASSERT_EQ(app_browser, browser());
  EXPECT_EQ(BrowserWindowInterface::Type::TYPE_APP, browser()->GetType());

  // Lock the app for OnTask.
  ash::boca::OnTaskLockedController::From(browser())->set_locked_for_on_task(
      true);
  const gfx::Rect& initial_bounds =
      browser()->GetWindow()->GetNativeWindow()->bounds();

  // Drag tab away from tab strip and verify the window bounds remain unchanged.
  TabStrip* const tab_strip = GetTabStripForBrowser(browser());
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(0)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(0),
                                gfx::Vector2d(0, GetDetachY(tab_strip) + 1)));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_EQ(browser()->GetWindow()->GetNativeWindow()->bounds(),
            initial_bounds);
}

INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTest,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse", "touch")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestWithFocusMode,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse", "touch")));
#else
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTest,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Bool(),
        /*input_source=*/::testing::Values("mouse")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestWithFocusMode,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Bool(),
        /*input_source=*/::testing::Values("mouse")));
#endif  // BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS)
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserInSeparateDisplayTabDragControllerTest,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestTouch,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("touch")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DifferentDeviceScaleFactorDisplayTabDragControllerTest,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserInSeparateDisplayAndCancelTabDragControllerTest,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestWithTabbedSystemApp,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse", "touch")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestWithOnTaskLocked,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse", "touch")));
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestWithTabbedWebApp,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse", "touch")));
#elif !BUILDFLAG(IS_MAC)
INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserTabDragControllerTestWithTabbedWebApp,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse")));
#endif

// TODO(crbug.com/409577362) : Fix test flakiness
#if !BUILDFLAG(IS_CHROMEOS)
class SideBySideTabDragControllerTest
    : public TabDragControllerInteractiveTestMixin<TabDragControllerTest> {
 public:
  bool IsTabInSplit(int index) {
    return GetTabStripForBrowser(browser())->tab_at(index)->split().has_value();
  }

  void AddTabs(BrowserWindowInterface* browser, int additional_tabs) {
    for (int i = 0; i < additional_tabs; i++) {
      auto* contents =
          chrome::AddSelectedTabWithURL(browser, GURL(url::kAboutBlankURL),
                                        ui::PAGE_TRANSITION_AUTO_TOPLEVEL);
      content::WaitForLoadStop(contents);
    }

    browser->GetWindow()->Show();
    StopAnimating(GetTabStripForBrowser(browser));
    BrowserView::GetBrowserViewForBrowser(browser)
        ->GetWidget()
        ->LayoutRootViewIfNecessary();
  }
};

IN_PROC_BROWSER_TEST_F(SideBySideTabDragControllerTest, DragBetweenSplitTab) {
  TabStrip* const tab_strip = GetTabStripForBrowser(browser());

  AddTabs(browser(), 2);

  split_tabs::SplitTabId id = split_tabs::SplitTabId::GenerateNew();
  tab_strip->OnSplitCreated({0, 1}, id);
  StopAnimating(tab_strip);

  Tab* const last_split_tab = tab_strip->tab_at(1);

  EXPECT_TRUE(IsTabInSplit(0));
  EXPECT_TRUE(IsTabInSplit(1));
  EXPECT_FALSE(IsTabInSplit(2));

  // Drag the last tab to the middle of the split tab.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(DragInputToCenter(last_split_tab));
  ASSERT_TRUE(ReleaseInput());
  StopAnimating(tab_strip);

  // The last tab should return to where it was since we can't drag a tab in
  // between a split.
  EXPECT_FALSE(IsTabInSplit(0));
  EXPECT_TRUE(IsTabInSplit(1));
  EXPECT_TRUE(IsTabInSplit(2));
}

// Flaky. https://crbug.com/40748225
IN_PROC_BROWSER_TEST_F(SideBySideTabDragControllerTest,
                       DragBetweenMultipleSplitTabs) {
  TabStrip* const tab_strip = GetTabStripForBrowser(browser());

  AddTabs(browser(), 4);

  split_tabs::SplitTabId first_id = split_tabs::SplitTabId::GenerateNew();
  tab_strip->OnSplitCreated({0, 1}, first_id);

  split_tabs::SplitTabId second_id = split_tabs::SplitTabId::GenerateNew();
  tab_strip->OnSplitCreated({2, 3}, second_id);

  StopAnimating(tab_strip);

  EXPECT_TRUE(IsTabInSplit(0));
  EXPECT_TRUE(IsTabInSplit(1));
  EXPECT_TRUE(IsTabInSplit(2));
  EXPECT_TRUE(IsTabInSplit(3));
  EXPECT_FALSE(IsTabInSplit(4));

  // Drag the last tab in between the two sets of split tabs.
  ASSERT_TRUE(PressInputAtCenter(tab_strip->tab_at(4)));
  ASSERT_TRUE(DragInputToCenter(tab_strip->tab_at(2)));
  ASSERT_TRUE(ReleaseInput());

  StopAnimating(tab_strip);

  // The single tab should be able to go in between the two sets of split tabs
  // since they belong to different splits.
  EXPECT_TRUE(IsTabInSplit(0));
  EXPECT_TRUE(IsTabInSplit(1));
  EXPECT_FALSE(IsTabInSplit(2));
  EXPECT_TRUE(IsTabInSplit(3));
  EXPECT_TRUE(IsTabInSplit(4));
}

#if BUILDFLAG(IS_MAC)
class DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop
    : public DetachToBrowserTabDragControllerTest {
 public:
  DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop() = default;
  DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop(
      const DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop&) =
      delete;
  DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop& operator=(
      const DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop&) =
      delete;
  ~DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop() override =
      default;

  void SetScreenInstance() override {
    mock_screen_.Init();
    // Primary display: 2000x1200
    mock_screen_.display_list().AddDisplay({1, gfx::Rect(0, 0, 2000, 1200)},
                                           display::DisplayList::Type::PRIMARY);
    // Secondary display: 1200x800, located at x=2000
    mock_screen_.display_list().AddDisplay(
        {2, gfx::Rect(2000, 0, 1200, 800)},
        display::DisplayList::Type::NOT_PRIMARY);
    ASSERT_EQ(2, display::Screen::Get()->GetNumDisplays());
  }

  void DragTabToSmallerSecondaryDisplayStep2(
      const gfx::Point& target_point,
      BrowserWindowInterface* source_browser,
      BrowserWindowInterface* detached_browser) {
    TabStrip* const source_tab_strip = GetTabStripForBrowser(source_browser);
    mock_screen_.set_cursor_position(target_point);
    ASSERT_TRUE(DragInputTo(target_point, GetWindowHint(source_tab_strip)));

    // Drag a little bit more to generate another drag event.
    gfx::Point target_point2 = target_point + gfx::Vector2d(10, 10);
    mock_screen_.set_cursor_position(target_point2);
    ASSERT_TRUE(DragInputTo(target_point2, GetWindowHint(source_tab_strip)));

    TabStrip* const detached_tab_strip =
        GetTabStripForBrowser(detached_browser);
    Tab* const dragged_tab = detached_tab_strip->tab_at(0);
    EXPECT_TRUE(dragged_tab->GetBoundsInScreen().Contains(target_point2));

    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(
            &DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop::
                DragTabToSmallerSecondaryDisplayStep3,
            base::Unretained(this)));
  }

  void DragTabToSmallerSecondaryDisplayStep3() { ASSERT_TRUE(ReleaseInput()); }

  void DragWindowToSmallerSecondaryDisplayStep2(
      const gfx::Point& target_point,
      BrowserWindowInterface* source_browser) {
    TabStrip* const source_tab_strip = GetTabStripForBrowser(source_browser);
    mock_screen_.set_cursor_position(target_point);
    ASSERT_TRUE(DragInputTo(target_point, GetWindowHint(source_tab_strip)));

    // Drag a little bit more to generate another drag event.
    gfx::Point target_point2 = target_point + gfx::Vector2d(10, 10);
    mock_screen_.set_cursor_position(target_point2);
    ASSERT_TRUE(DragInputTo(target_point2, GetWindowHint(source_tab_strip)));

    Tab* const dragged_tab = source_tab_strip->tab_at(0);
    EXPECT_TRUE(dragged_tab->GetBoundsInScreen().Contains(target_point2));

    base::SingleThreadTaskRunner::GetCurrentDefault()->PostTask(
        FROM_HERE,
        base::BindOnce(
            &DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop::
                DragWindowToSmallerSecondaryDisplayStep3,
            base::Unretained(this)));
  }

  void DragWindowToSmallerSecondaryDisplayStep3() {
    ASSERT_TRUE(ReleaseInput());
  }

 protected:
  class MockScreen : public display::ScreenBase {
   public:
    MockScreen() = default;
    ~MockScreen() override { display::Screen::SetScreenInstance(nullptr); }

    void Init() { display::Screen::SetScreenInstance(this); }

    display::Display GetDisplayNearestWindow(
        gfx::NativeWindow window) const override {
      views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window);
      if (widget) {
        return GetDisplayMatching(widget->GetWindowBoundsInScreen());
      }
      return GetPrimaryDisplay();
    }

    gfx::Point GetCursorScreenPoint() override { return cursor_position_; }

    void set_cursor_position(const gfx::Point& point) {
      cursor_position_ = point;
    }

   private:
    gfx::Point cursor_position_;
  };

  MockScreen mock_screen_;
};

// Drags a tab to a secondary smaller display and releases.
// Verifies that the new window created on the secondary display is downsized
// to fit inside the work area of that secondary display.
IN_PROC_BROWSER_TEST_P(
    DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop,
    DragTabToSmallerSecondaryDisplay) {
  AddTabsAndResetBrowser(browser(), 1);

  // Size the source browser window to 1600x1000, which is larger than the
  // secondary display's work area (1200x800).
  browser()->GetWindow()->SetBounds(gfx::Rect(0, 0, 1600, 1000));

  display::Screen* const screen = display::Screen::Get();
  display::Display second_display = ui_test_utils::GetSecondaryDisplay(screen);
  ASSERT_EQ(second_display.id(), 2);

  mock_screen_.set_cursor_position(GetCenterInScreenCoordinates(
      GetTabStripForBrowser(browser())->tab_at(1)));

  // Target point is on the secondary screen.
  const gfx::Point target(second_display.bounds().x() + 200,
                          second_display.bounds().y() + 200);
  ASSERT_TRUE(second_display.bounds().Contains(target));

  // Perform drag to detach, then continue dragging to target on secondary
  // display.
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(),
      base::BindOnce(
          &DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop::
              DragTabToSmallerSecondaryDisplayStep2,
          base::Unretained(this), target),
      1);

  ASSERT_TRUE(new_browser);

  // Verify that the new browser's bounds fit within the secondary display's
  // work area.
  gfx::Rect new_bounds = new_browser->GetWindow()->GetBounds();
  EXPECT_LE(new_bounds.width(), second_display.work_area().width());
  EXPECT_LE(new_bounds.height(), second_display.work_area().height());
}

// Drags a single-tab browser window to a secondary smaller display and
// releases. Verifies that the browser window on the secondary display is
// downsized to fit inside the work area of that secondary display.
IN_PROC_BROWSER_TEST_P(
    DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop,
    DragWindowToSmallerSecondaryDisplay) {
  // Ensure the browser has exactly 1 tab.
  ASSERT_EQ(1, browser()->GetTabStripModel()->count());
  TabStrip* tab_strip = GetTabStripForBrowser(browser());

  // Size the browser window to 1600x1000, which is larger than the
  // secondary display's work area (1200x800).
  browser()->GetWindow()->SetBounds(gfx::Rect(0, 0, 1600, 1000));

  display::Screen* const screen = display::Screen::Get();
  display::Display second_display = ui_test_utils::GetSecondaryDisplay(screen);
  ASSERT_EQ(second_display.id(), 2);

  mock_screen_.set_cursor_position(
      GetCenterInScreenCoordinates(tab_strip->tab_at(0)));

  // Target point is on the secondary screen.
  const gfx::Point target(second_display.bounds().x() + 200,
                          second_display.bounds().y() + 200);
  ASSERT_TRUE(second_display.bounds().Contains(target));

  // Perform drag (which will drag the window since it's a single-tab window),
  // then continue dragging to target on secondary display.
  DragTabAndNotify(
      tab_strip,
      base::BindOnce(
          &DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop::
              DragWindowToSmallerSecondaryDisplayStep2,
          base::Unretained(this), target, browser()),
      0);

  // Verify that the browser's bounds fit within the secondary display's work
  // area.
  gfx::Rect new_bounds = browser()->GetWindow()->GetBounds();
  EXPECT_LE(new_bounds.width(), second_display.work_area().width());
  EXPECT_LE(new_bounds.height(), second_display.work_area().height());
}

// Drags a tab to a secondary display with a work area smaller than its physical
// size (insets) and releases. Verifies that the new window created on the
// secondary display is downsized to fit inside the WORK AREA of that secondary
// display, not the physical bounds.
IN_PROC_BROWSER_TEST_P(
    DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop,
    DragTabToSecondaryDisplayWithWorkAreaInsets) {
  AddTabsAndResetBrowser(browser(), 1);

  // Size the source browser window to 1600x1000, which is larger than the
  // secondary display's bounds (1200x800) and work area (1200x600).
  browser()->GetWindow()->SetBounds(gfx::Rect(0, 0, 1600, 1000));

  display::Screen* const screen = display::Screen::Get();
  display::Display second_display = ui_test_utils::GetSecondaryDisplay(screen);
  ASSERT_EQ(second_display.id(), 2);

  // Update the secondary display to have work area insets (e.g. y-offset of
  // 100, height of 600).
  second_display.set_work_area(gfx::Rect(2000, 100, 1200, 600));
  mock_screen_.display_list().UpdateDisplay(second_display);

  mock_screen_.set_cursor_position(GetCenterInScreenCoordinates(
      GetTabStripForBrowser(browser())->tab_at(1)));

  // Target point is on the secondary screen inside its work area.
  const gfx::Point target(second_display.bounds().x() + 200,
                          second_display.bounds().y() + 200);
  ASSERT_TRUE(second_display.work_area().Contains(target));

  // Perform drag to detach, then continue dragging to target on secondary
  // display.
  BrowserWindowInterface* const new_browser = DragTabForDetachAndNotify(
      browser(),
      base::BindOnce(
          &DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop::
              DragTabToSmallerSecondaryDisplayStep2,
          base::Unretained(this), target),
      1);

  ASSERT_TRUE(new_browser);

  // Verify that the new browser's bounds fit within the secondary display's
  // work area, accounting for kMaximizedWindowInset (10 DIPs) on each side:
  // Expected max width = 1200 - 2 * 10 = 1180
  // Expected max height = 600 - 2 * 10 = 580
  gfx::Rect new_bounds = new_browser->GetWindow()->GetBounds();
  EXPECT_LE(new_bounds.width(), 1180);
  EXPECT_LE(new_bounds.height(), 580);
}

INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    DetachToBrowserInSeparateDisplayTabDragControllerTestDesktop,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("mouse")));
#endif  // BUILDFLAG(IS_MAC)
#endif  // !BUILDFLAG(IS_CHROMEOS)

#if BUILDFLAG(IS_CHROMEOS)

namespace {
class TabDragControllerTabletModeTest
    : public DetachToBrowserTabDragControllerTest {
 public:
  TabDragControllerTabletModeTest() = default;
  TabDragControllerTabletModeTest(const TabDragControllerTabletModeTest&) =
      delete;
  TabDragControllerTabletModeTest& operator=(
      const TabDragControllerTabletModeTest&) = delete;
  virtual ~TabDragControllerTabletModeTest() = default;

  // DetachToBrowserTabDragControllerTest:
  void SetUpOnMainThread() override {
    DetachToBrowserTabDragControllerTest::SetUpOnMainThread();

    ash::TabletMode::Waiter waiter(true);
    ash::TabletMode::Get()->SetEnabledForTest(true);
    waiter.Wait();
  }

  bool InTabletSplitViewMode(BrowserWindowInterface* primary,
                             BrowserWindowInterface* secondary) {
    return split_view_controller()->InTabletSplitViewMode() &&
           split_view_controller()->state() ==
               ash::SplitViewController::State::kBothSnapped &&
           split_view_controller()->primary_window() ==
               primary->GetWindow()->GetNativeWindow() &&
           split_view_controller()->secondary_window() ==
               secondary->GetWindow()->GetNativeWindow();
  }

  bool InTabletSplitViewModeWithOverviewAsSecondary(
      BrowserWindowInterface* primary) {
    return split_view_controller()->InTabletSplitViewMode() &&
           split_view_controller()->state() ==
               ash::SplitViewController::State::kPrimarySnapped &&
           split_view_controller()->primary_window() ==
               primary->GetWindow()->GetNativeWindow();
  }

  bool DragInactive(size_t num_browsers) {
    return !TabDragController::IsActive() &&
           GetAllBrowserWindowInterfaces().size() == num_browsers;
  }

  bool IsDraggingInfoCleared(BrowserWindowInterface* browser) {
    TabStrip* tab_strip = GetTabStripForBrowser(browser);
    return !IsDragSessionActive(tab_strip) &&
           IsTabDraggingInfoCleared(tab_strip);
  }

  bool IsFloated(BrowserWindowInterface* browser) {
    return ash::WindowState::Get(browser->GetWindow()->GetNativeWindow())
        ->IsFloated();
  }

  bool IsInOverview(BrowserWindowInterface* browser) {
    return ash::GetOverviewSession() &&
           ash::GetOverviewSession()->IsWindowInOverview(
               browser->GetWindow()->GetNativeWindow());
  }

  ash::SplitViewController* split_view_controller() {
    return ash::SplitViewController::Get(root_);
  }

  // Invoked from TabDragController's nested run loop.
  void MaybeDragThenRelease(TabStrip* source_tab_strip,
                            std::optional<gfx::Point> destination_in_screen) {
    MaybeDragThenMaybeRelease(source_tab_strip, destination_in_screen, true);
  }
  void MaybeDragThenHold(TabStrip* source_tab_strip,
                         std::optional<gfx::Point> destination_in_screen) {
    MaybeDragThenMaybeRelease(source_tab_strip, destination_in_screen, false);
  }

  void MaybeDragThenMaybeRelease(
      TabStrip* source_tab_strip,
      std::optional<gfx::Point> destination_in_screen,
      bool release = true) {
    EXPECT_TRUE(TabDragController::IsActive());

    // Source tab strip drag session is already finished.
    EXPECT_FALSE(IsDragSessionActive(source_tab_strip));
    EXPECT_TRUE(IsTabDraggingInfoCleared(source_tab_strip));

    BrowserWindowInterface* source_browser =
        source_tab_strip->GetBrowserWindowInterface();
    BrowserWindowInterface* drag_browser =
        GetLastActiveBrowserWindowInterfaceWithAnyProfile();
    EXPECT_NE(source_browser, drag_browser);

    aura::Window* drag_window = drag_browser->GetWindow()->GetNativeWindow();
    aura::Window* source_window =
        source_browser->GetWindow()->GetNativeWindow();
    EXPECT_EQ(source_window,
              ash::window_util::GetTabDraggingSourceWindowState(drag_window)
                  ->window());

    TabStrip* drag_tab_strip = GetTabStripForBrowser(drag_browser);
    EXPECT_TRUE(IsDragSessionActive(drag_tab_strip));
    EXPECT_TRUE(IsTabDraggingInfoSet(drag_tab_strip));

    EXPECT_EQ(source_browser->GetTabStripModel()->count(), 1);
    EXPECT_EQ(drag_browser->GetTabStripModel()->count(), 1);

    if (destination_in_screen.has_value()) {
      ASSERT_TRUE(DragInputTo(*destination_in_screen, gfx::NativeWindow()));
    }

    if (release) {
      ASSERT_TRUE(ReleaseInput());
    }
  }
};
}  // namespace

// In tablet mode, drag a tab out of a maximized browser and drop it below the
// tab strip. The drag browser persists and becomes maximized.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest, DragFromMaximized) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  Tab* first_tab = tab_strip->tab_at(0);
  EXPECT_TRUE(browser()->IsActive());
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip, std::nullopt),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(2); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 2u);
  BrowserWindowInterface* drag_browser =
      ui_test_utils::GetBrowserNotInSet({browser()});
  EXPECT_TRUE(IsDraggingInfoCleared(drag_browser));
  EXPECT_TRUE(browser()->GetWindow()->IsMaximized());
  EXPECT_TRUE(drag_browser->GetWindow()->IsMaximized());
  EXPECT_TRUE(drag_browser->IsActive());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(drag_browser->GetTabStripModel()), "0");
}

// In tablet mode, drag a tab out of a fullscreen browser and drop it below the
// tab strip. The drag browser persists and becomes fullscreen.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest, DragFromFullscreen) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  chrome::ToggleFullscreenMode(browser());
  EXPECT_TRUE(browser()->GetWindow()->IsFullscreen());

  // Forcibly reveal the tabstrip immediately.
  // TODO(crbug.com/464354169): Use focus to reveal the immersive frame.
  ImmersiveModeController* immersive_controller =
      ImmersiveModeController::From(browser());
  std::unique_ptr<ImmersiveRevealedLock> lock =
      immersive_controller->GetRevealedLock(
          ImmersiveModeController::ANIMATE_REVEAL_NO);
  EXPECT_TRUE(immersive_controller->IsRevealed());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  EXPECT_TRUE(tab_strip->GetVisible());
  Tab* first_tab = tab_strip->tab_at(0);
  EXPECT_TRUE(browser()->IsActive());
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip, std::nullopt),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  EXPECT_TRUE(base::test::RunUntil([&] { return DragInactive(2); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 2u);
  BrowserWindowInterface* drag_browser =
      ui_test_utils::GetBrowserNotInSet({browser()});
  EXPECT_TRUE(IsDraggingInfoCleared(drag_browser));
  EXPECT_TRUE(browser()->GetWindow()->IsFullscreen());
  EXPECT_TRUE(drag_browser->GetWindow()->IsFullscreen());
  EXPECT_TRUE(drag_browser->IsActive());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(drag_browser->GetTabStripModel()), "0");
}

// In tablet mode, drag a tab out of a floated browser and drop it below the tab
// strip. The drag browser persists and becomes maximized.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest, DragFromFloated) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  ash::Shell::Get()->float_controller()->ToggleFloat(
      browser()->GetWindow()->GetNativeWindow());
  EXPECT_TRUE(IsFloated(browser()));

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  Tab* first_tab = tab_strip->tab_at(0);
  EXPECT_TRUE(browser()->IsActive());
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip, std::nullopt),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(2); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 2u);
  BrowserWindowInterface* drag_browser =
      ui_test_utils::GetBrowserNotInSet({browser()});
  EXPECT_TRUE(IsDraggingInfoCleared(drag_browser));
  EXPECT_TRUE(IsFloated(browser()));
  EXPECT_TRUE(drag_browser->GetWindow()->IsMaximized());
  EXPECT_TRUE(drag_browser->IsActive());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(drag_browser->GetTabStripModel()), "0");
}

// In tablet mode, drag a tab out of a maximized browser and drop it on the edge
// of the screen. The drag browser persists and gets snapped to the side (split
// view), with the other side showing partial overview.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest,
                       DragFromMaximizedToSnapTriggerArea) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  Tab* first_tab = tab_strip->tab_at(0);
  EXPECT_TRUE(browser()->IsActive());
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  gfx::Rect work_area =
      display::Screen::Get()
          ->GetDisplayNearestWindow(browser()->GetWindow()->GetNativeWindow())
          .work_area();
  gfx::Point snap_primary_trigger(0, work_area.CenterPoint().y());
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip, snap_primary_trigger),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(2); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 2u);
  BrowserWindowInterface* drag_browser =
      ui_test_utils::GetBrowserNotInSet({browser()});
  EXPECT_TRUE(IsDraggingInfoCleared(drag_browser));

  EXPECT_TRUE(browser()->GetWindow()->IsMaximized());
  EXPECT_TRUE(drag_browser->IsActive());
  ASSERT_TRUE(base::test::RunUntil([&] {
    return InTabletSplitViewModeWithOverviewAsSecondary(drag_browser);
  }));
  EXPECT_TRUE(IsInOverview(browser()));
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(drag_browser->GetTabStripModel()), "0");
}

// In tablet mode, drag a tab out of a fullscreen browser and drop it on the
// edge of the screen. The drag browser persists and gets snapped to the side
// (split view), with the other side showing partial overview.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest,
                       DragFromFullscreenToSnapTriggerArea) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  chrome::ToggleFullscreenMode(browser());
  EXPECT_TRUE(browser()->GetWindow()->IsFullscreen());

  // Forcibly reveal the tabstrip immediately.
  ImmersiveModeController* immersive_controller =
      ImmersiveModeController::From(browser());
  std::unique_ptr<ImmersiveRevealedLock> lock =
      immersive_controller->GetRevealedLock(
          ImmersiveModeController::ANIMATE_REVEAL_NO);
  EXPECT_TRUE(immersive_controller->IsRevealed());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  EXPECT_TRUE(tab_strip->GetVisible());
  Tab* first_tab = tab_strip->tab_at(0);
  EXPECT_TRUE(browser()->IsActive());
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  gfx::Rect work_area =
      display::Screen::Get()
          ->GetDisplayNearestWindow(browser()->GetWindow()->GetNativeWindow())
          .work_area();
  gfx::Point snap_primary_trigger(0, work_area.CenterPoint().y());
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip, snap_primary_trigger),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(2); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 2u);
  BrowserWindowInterface* drag_browser =
      ui_test_utils::GetBrowserNotInSet({browser()});
  EXPECT_TRUE(IsDraggingInfoCleared(drag_browser));

  EXPECT_TRUE(browser()->GetWindow()->IsFullscreen());
  EXPECT_TRUE(drag_browser->IsActive());
  ASSERT_TRUE(base::test::RunUntil([&] {
    return InTabletSplitViewModeWithOverviewAsSecondary(drag_browser);
  }));
  EXPECT_TRUE(IsInOverview(browser()));
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(drag_browser->GetTabStripModel()), "0");
}

// In tablet mode, drag a tab out of a floated browser and drop it on the
// edge of the screen. The drag browser persists and gets snapped to the side
// (split view), with the other side showing partial overview.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest,
                       DragFromFloatedToSnapTriggerArea) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  ash::Shell::Get()->float_controller()->ToggleFloat(
      browser()->GetWindow()->GetNativeWindow());
  EXPECT_TRUE(IsFloated(browser()));

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  EXPECT_TRUE(tab_strip->GetVisible());
  Tab* first_tab = tab_strip->tab_at(0);
  EXPECT_TRUE(browser()->IsActive());
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  gfx::Rect work_area =
      display::Screen::Get()
          ->GetDisplayNearestWindow(browser()->GetWindow()->GetNativeWindow())
          .work_area();
  gfx::Point snap_primary_trigger(0, work_area.CenterPoint().y());
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip, snap_primary_trigger),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(2); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 2u);
  BrowserWindowInterface* drag_browser =
      ui_test_utils::GetBrowserNotInSet({browser()});
  EXPECT_TRUE(IsDraggingInfoCleared(drag_browser));

  EXPECT_TRUE(IsFloated(browser()));
  EXPECT_TRUE(drag_browser->IsActive());
  ASSERT_TRUE(base::test::RunUntil([&] {
    return InTabletSplitViewModeWithOverviewAsSecondary(drag_browser);
  }));
  EXPECT_TRUE(IsInOverview(browser()));
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(drag_browser->GetTabStripModel()), "0");
}

// In tablet mode, drag a tab out of a maximized browser and drop it on an
// existing floated browser's tab strip. The drag browser disappears and the tab
// gets inserted into the floated browser.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest,
                       DragFromMaximizedToFloatedTabStrip) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");
  EXPECT_TRUE(browser()->IsActive());

  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  ResetIDs(browser2->GetTabStripModel(), 10);
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "10");
  ash::Shell::Get()->float_controller()->ToggleFloat(
      browser2->GetWindow()->GetNativeWindow());
  EXPECT_TRUE(IsFloated(browser2));

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  Tab* first_tab = tab_strip->tab_at(0);

  ui_test_utils::BrowserActivationWaiter activation_waiter(browser());
  browser()->GetWindow()->Activate();
  activation_waiter.WaitForActivation();
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  Tab* first_tab2 = tab_strip2->tab_at(0);
  gfx::Point first_tab2_center = first_tab2->GetBoundsInScreen().CenterPoint();

  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip, first_tab2_center),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(2); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 2u);
  EXPECT_TRUE(browser()->GetWindow()->IsMaximized());
  EXPECT_TRUE(IsFloated(browser2));
  EXPECT_TRUE(browser2->IsActive());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "0 10");
}

// In tablet mode, drag a tab out of a fullscreen browser and drop it on an
// existing floated browser's tab strip. The drag browser disappears and the tab
// gets inserted into the floated browser.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest,
                       DragFromFullscreenToFloatedTabStrip) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  ResetIDs(browser2->GetTabStripModel(), 10);
  ash::Shell::Get()->float_controller()->ToggleFloat(
      browser2->GetWindow()->GetNativeWindow());
  EXPECT_TRUE(IsFloated(browser2));

  chrome::ToggleFullscreenMode(browser());
  EXPECT_TRUE(browser()->GetWindow()->IsFullscreen());

  ui_test_utils::BrowserActivationWaiter activation_waiter(browser());
  browser()->GetWindow()->Activate();
  activation_waiter.WaitForActivation();

  // Forcibly reveal the tabstrip immediately.
  ImmersiveModeController* immersive_controller =
      ImmersiveModeController::From(browser());
  std::unique_ptr<ImmersiveRevealedLock> lock =
      immersive_controller->GetRevealedLock(
          ImmersiveModeController::ANIMATE_REVEAL_NO);
  EXPECT_TRUE(immersive_controller->IsRevealed());

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  EXPECT_TRUE(tab_strip->GetVisible());
  Tab* first_tab = tab_strip->tab_at(0);
  EXPECT_TRUE(browser()->IsActive());
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  Tab* first_tab2 = tab_strip2->tab_at(0);
  gfx::Point first_tab2_center = first_tab2->GetBoundsInScreen().CenterPoint();

  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip, first_tab2_center),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(2); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 2u);
  EXPECT_TRUE(browser()->GetWindow()->IsFullscreen());
  EXPECT_TRUE(IsFloated(browser2));
  EXPECT_TRUE(browser2->IsActive());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "0 10");
}

// In tablet mode, drag a tab out of a snapped browser and drop it below the
// tab strip. The drag browser persists and becomes snapped, replacing the
// source browser in the split view.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest, DragFromSnapped) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  ResetIDs(browser2->GetTabStripModel(), 10);
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "10");

  split_view_controller()->SnapWindow(browser()->GetWindow()->GetNativeWindow(),
                                      ash::SnapPosition::kPrimary);
  split_view_controller()->SnapWindow(browser2->GetWindow()->GetNativeWindow(),
                                      ash::SnapPosition::kSecondary);
  EXPECT_TRUE(InTabletSplitViewMode(browser(), browser2));

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  Tab* first_tab = tab_strip->tab_at(0);

  ui_test_utils::BrowserActivationWaiter activation_waiter(browser());
  browser()->GetWindow()->Activate();
  activation_waiter.WaitForActivation();
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip, std::nullopt),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(3); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 3u);
  BrowserWindowInterface* drag_browser =
      ui_test_utils::GetBrowserNotInSet({browser(), browser2});
  EXPECT_TRUE(IsDraggingInfoCleared(drag_browser));
  EXPECT_TRUE(InTabletSplitViewMode(drag_browser, browser2));
  EXPECT_TRUE(drag_browser->IsActive());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "10");
  EXPECT_EQ(IDString(drag_browser->GetTabStripModel()), "0");
}

// In tablet mode, drag a tab out of a snapped browser and drop it on the other
// side of split view, where another browser is shown (drop outside of that
// browser's tab strip). The drag browser persists and becomes snapped,
// replacing the other browser in the split view.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest,
                       DragFromSnappedToOtherSnapped) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  ResetIDs(browser2->GetTabStripModel(), 10);
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "10");

  split_view_controller()->SnapWindow(browser()->GetWindow()->GetNativeWindow(),
                                      ash::SnapPosition::kPrimary);
  split_view_controller()->SnapWindow(browser2->GetWindow()->GetNativeWindow(),
                                      ash::SnapPosition::kSecondary);
  EXPECT_TRUE(InTabletSplitViewMode(browser(), browser2));

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  Tab* first_tab = tab_strip->tab_at(0);

  ui_test_utils::BrowserActivationWaiter activation_waiter(browser());
  browser()->GetWindow()->Activate();
  activation_waiter.WaitForActivation();
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  gfx::Rect left_side, right_side;
  display::Screen::Get()
      ->GetDisplayNearestWindow(browser()->GetWindow()->GetNativeWindow())
      .work_area()
      .SplitVertically(left_side, right_side);

  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip,
                     right_side.CenterPoint()),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(3); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 3u);
  BrowserWindowInterface* drag_browser =
      ui_test_utils::GetBrowserNotInSet({browser(), browser2});
  EXPECT_TRUE(IsDraggingInfoCleared(drag_browser));
  EXPECT_TRUE(InTabletSplitViewMode(browser(), drag_browser));
  EXPECT_TRUE(drag_browser->IsActive());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "10");
  EXPECT_EQ(IDString(drag_browser->GetTabStripModel()), "0");
}

// In tablet mode, drag a tab out of a snapped browser and drop it on the other
// side of split view, where overview is shown. The drag browser persists and
// becomes snapped, replacing the overview in the split view.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest,
                       DragFromSnappedToOverview) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  ResetIDs(browser2->GetTabStripModel(), 10);
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "10");

  split_view_controller()->SnapWindow(browser()->GetWindow()->GetNativeWindow(),
                                      ash::SnapPosition::kPrimary);
  EXPECT_TRUE(InTabletSplitViewModeWithOverviewAsSecondary(browser()));
  EXPECT_TRUE(IsInOverview(browser2));

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  Tab* first_tab = tab_strip->tab_at(0);

  ui_test_utils::BrowserActivationWaiter activation_waiter(browser());
  browser()->GetWindow()->Activate();
  activation_waiter.WaitForActivation();
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  gfx::Rect left_side, right_side;
  display::Screen::Get()
      ->GetDisplayNearestWindow(browser()->GetWindow()->GetNativeWindow())
      .work_area()
      .SplitVertically(left_side, right_side);

  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip,
                     right_side.CenterPoint()),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(3); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 3u);
  BrowserWindowInterface* drag_browser =
      ui_test_utils::GetBrowserNotInSet({browser(), browser2});
  EXPECT_TRUE(IsDraggingInfoCleared(drag_browser));
  EXPECT_TRUE(split_view_controller()->InTabletSplitViewMode());
  EXPECT_TRUE(InTabletSplitViewMode(browser(), drag_browser));
  EXPECT_TRUE(drag_browser->IsActive());
  EXPECT_FALSE(ash::GetOverviewSession());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "10");
  EXPECT_EQ(IDString(drag_browser->GetTabStripModel()), "0");
}

// In tablet mode, drag a tab out of a snapped browser and drop it on the tab
// strip of the browser on the other side of split view. The drag browser
// disappears and the tab gets inserted into the other browser.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest,
                       DragFromSnappedToOtherSnappedTabStrip) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  ResetIDs(browser2->GetTabStripModel(), 10);
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "10");

  split_view_controller()->SnapWindow(browser()->GetWindow()->GetNativeWindow(),
                                      ash::SnapPosition::kPrimary);
  split_view_controller()->SnapWindow(browser2->GetWindow()->GetNativeWindow(),
                                      ash::SnapPosition::kSecondary);
  EXPECT_TRUE(InTabletSplitViewMode(browser(), browser2));

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  Tab* first_tab = tab_strip->tab_at(0);

  ui_test_utils::BrowserActivationWaiter activation_waiter(browser());
  browser()->GetWindow()->Activate();
  activation_waiter.WaitForActivation();
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  Tab* first_tab2 = tab_strip2->tab_at(0);
  gfx::Point first_tab2_center = first_tab2->GetBoundsInScreen().CenterPoint();

  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip, first_tab2_center),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(2); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 2u);
  EXPECT_TRUE(InTabletSplitViewMode(browser(), browser2));
  EXPECT_TRUE(browser2->IsActive());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "0 10");
}

// In tablet mode, drag a tab group out of a snapped browser and drop it on the
// tab strip of the browser on the other side of split view. The drag browser
// disappears and the tab gets inserted into the other browser.
IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest,
                       DragGroupFromSnappedToOtherSnappedTabStrip) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  ResetIDs(browser2->GetTabStripModel(), 10);
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "10");

  split_view_controller()->SnapWindow(browser()->GetWindow()->GetNativeWindow(),
                                      ash::SnapPosition::kPrimary);
  split_view_controller()->SnapWindow(browser2->GetWindow()->GetNativeWindow(),
                                      ash::SnapPosition::kSecondary);
  EXPECT_TRUE(InTabletSplitViewMode(browser(), browser2));

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  Tab* first_tab = tab_strip->tab_at(0);

  ui_test_utils::BrowserActivationWaiter activation_waiter(browser());
  browser()->GetWindow()->Activate();
  activation_waiter.WaitForActivation();
  EXPECT_TRUE(PressInputAtCenter(first_tab));

  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  Tab* first_tab2 = tab_strip2->tab_at(0);
  gfx::Point first_tab2_center = first_tab2->GetBoundsInScreen().CenterPoint();

  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip, first_tab2_center),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(2); }));

  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 2u);
  EXPECT_TRUE(InTabletSplitViewMode(browser(), browser2));
  EXPECT_TRUE(browser2->IsActive());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "0 10");
}

IN_PROC_BROWSER_TEST_P(TabDragControllerTabletModeTest, DoubleDetach) {
  AddTabsAndResetBrowser(browser(), 1);
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");

  BrowserWindowInterface* browser2 = CreateBrowser(browser()->GetProfile());
  ResetIDs(browser2->GetTabStripModel(), 10);
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "10");

  split_view_controller()->SnapWindow(browser()->GetWindow()->GetNativeWindow(),
                                      ash::SnapPosition::kPrimary);
  split_view_controller()->SnapWindow(browser2->GetWindow()->GetNativeWindow(),
                                      ash::SnapPosition::kSecondary);
  EXPECT_TRUE(InTabletSplitViewMode(browser(), browser2));

  TabStrip* tab_strip = GetTabStripForBrowser(browser());
  Tab* first_tab = tab_strip->tab_at(0);
  gfx::Point first_tab_center = first_tab->GetBoundsInScreen().CenterPoint();

  ui_test_utils::BrowserActivationWaiter activation_waiter(browser());
  browser()->GetWindow()->Activate();
  activation_waiter.WaitForActivation();
  ASSERT_TRUE(PressInputAtCenter(first_tab));

  TabStrip* tab_strip2 = GetTabStripForBrowser(browser2);
  Tab* first_tab2 = tab_strip2->tab_at(0);
  gfx::Point first_tab2_center = first_tab2->GetBoundsInScreen().CenterPoint();

  // Drag from browser() into browser2, but don't release.
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      first_tab,
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenHold,
                     base::Unretained(this), tab_strip, first_tab2_center),
      gfx::Vector2d(0, GetDetachY(tab_strip))));
  ASSERT_TRUE(base::test::RunUntil(
      [&] { return IDString(browser2->GetTabStripModel()) == "0 10"; }));
  EXPECT_TRUE(TabDragController::IsActive());
  EXPECT_EQ(GetAllBrowserWindowInterfaces().size(), 2u);
  EXPECT_TRUE(InTabletSplitViewMode(browser(), browser2));
  EXPECT_TRUE(browser2->IsActive());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "1");

  // Drag out of browser2 back into browser(), and release there.
  ASSERT_TRUE(DragInputToCenterNotifyWhenDone(
      tab_strip2->tab_at(0),
      base::BindOnce(&TabDragControllerTabletModeTest::MaybeDragThenRelease,
                     base::Unretained(this), tab_strip2, first_tab_center),
      gfx::Vector2d(0, GetDetachY(tab_strip2))));
  ASSERT_TRUE(base::test::RunUntil([&] { return DragInactive(2); }));
  EXPECT_TRUE(InTabletSplitViewMode(browser(), browser2));
  EXPECT_TRUE(browser()->IsActive());
  EXPECT_EQ(IDString(browser()->GetTabStripModel()), "0 1");
  EXPECT_EQ(IDString(browser2->GetTabStripModel()), "10");
}

INSTANTIATE_TEST_SUITE_P(
    TabDragging,
    TabDragControllerTabletModeTest,
    ::testing::Combine(
        /*kTearOffWebAppTabOpensWebAppWindow=*/::testing::Values(false),
        /*input_source=*/::testing::Values("touch")));

#endif  // BUILDFLAG(IS_CHROMEOS)
