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

#include "base/memory/raw_ptr.h"
#include "build/build_config.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/create_browser_window.h"
#include "chrome/browser/ui/tabs/split_tab_metrics.h"
#include "chrome/browser/ui/view_ids.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/multi_contents_resize_area.h"
#include "chrome/browser/ui/views/frame/multi_contents_view.h"
#include "chrome/browser/ui/views/frame/multi_contents_view_mini_toolbar.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 "content/public/test/browser_test.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "ui/views/controls/webview/webview.h"
#include "ui/views/focus/focus_manager.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
#include "url/gurl.h"

const char kSimplePage[] = "/focus/page_with_focus.html";

class BrowserViewFocusTest : public InProcessBrowserTest {
 public:
  BrowserViewFocusTest() = default;

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

  ~BrowserViewFocusTest() override = default;

  bool IsViewFocused(ViewID vid) {
    return ui_test_utils::IsViewFocused(browser(), vid);
  }

  void TestSplitTabFocusOrder() {
    gfx::NativeWindow window = browser()->GetWindow()->GetNativeWindow();
    views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window);
    views::FocusManager* focus_manager = widget->GetFocusManager();

    std::vector<raw_ptr<ContentsContainerView, DanglingUntriaged>>
        contents_container_views =
            BrowserView::GetBrowserViewForBrowser(browser())
                ->GetContentsContainerViews();
    ASSERT_EQ(2, contents_container_views.size());

    // Start from the view prior to the left contents web view in the focus
    // order. This should be somewhere outside of the contents container, but
    // where it is depends on the platform.
    focus_manager->SetFocusedView(contents_container_views[0]->contents_view());
    focus_manager->AdvanceFocus(true);
    views::View* start_view = focus_manager->GetFocusedView();
    ASSERT_FALSE(BrowserView::GetBrowserViewForBrowser(browser())
                     ->contents_container()
                     ->Contains(start_view));

    // Start advancing focus forwards.
    focus_manager->AdvanceFocus(false);
    EXPECT_EQ(0, browser()->GetTabStripModel()->active_index());
    EXPECT_EQ(focus_manager->GetFocusedView(),
              contents_container_views[0]->contents_view());

    focus_manager->AdvanceFocus(false);
    EXPECT_EQ(0, browser()->GetTabStripModel()->active_index());
    EXPECT_TRUE(contents_container_views[0]->mini_toolbar()->Contains(
        focus_manager->GetFocusedView()));

    focus_manager->AdvanceFocus(false);
    EXPECT_EQ(0, browser()->GetTabStripModel()->active_index());
    EXPECT_TRUE(BrowserView::GetBrowserViewForBrowser(browser())
                    ->multi_contents_view()
                    ->resize_area_for_testing()
                    ->Contains(focus_manager->GetFocusedView()));

    focus_manager->AdvanceFocus(false);
    EXPECT_EQ(1, browser()->GetTabStripModel()->active_index());
    EXPECT_EQ(focus_manager->GetFocusedView(),
              contents_container_views[1]->contents_view());

    focus_manager->AdvanceFocus(false);
    EXPECT_EQ(1, browser()->GetTabStripModel()->active_index());
    EXPECT_TRUE(contents_container_views[1]->mini_toolbar()->Contains(
        focus_manager->GetFocusedView()));

    // Focus has advanced past the right tab's mini toolbar button. This should
    // be somewhere outside of the contents container, but where it is depends
    // on the platform.
    focus_manager->AdvanceFocus(false);
    EXPECT_EQ(1, browser()->GetTabStripModel()->active_index());
    ASSERT_FALSE(BrowserView::GetBrowserViewForBrowser(browser())
                     ->contents_container()
                     ->Contains(focus_manager->GetFocusedView()));

    // Start advancing focus backwards.
    focus_manager->AdvanceFocus(true);
    EXPECT_EQ(1, browser()->GetTabStripModel()->active_index());
    EXPECT_TRUE(contents_container_views[1]->mini_toolbar()->Contains(
        focus_manager->GetFocusedView()));

    focus_manager->AdvanceFocus(true);
    EXPECT_EQ(1, browser()->GetTabStripModel()->active_index());
    EXPECT_EQ(focus_manager->GetFocusedView(),
              contents_container_views[1]->contents_view());

    focus_manager->AdvanceFocus(true);
    EXPECT_EQ(1, browser()->GetTabStripModel()->active_index());
    EXPECT_TRUE(BrowserView::GetBrowserViewForBrowser(browser())
                    ->multi_contents_view()
                    ->resize_area_for_testing()
                    ->Contains(focus_manager->GetFocusedView()));

    focus_manager->AdvanceFocus(true);
    // The right tab is still focused here, because we entered the left tab's
    // mini toolbar in reverse, so we have not yet focused the left contents web
    // view.
    EXPECT_EQ(1, browser()->GetTabStripModel()->active_index());
    EXPECT_TRUE(contents_container_views[0]->mini_toolbar()->Contains(
        focus_manager->GetFocusedView()));

    focus_manager->AdvanceFocus(true);
    EXPECT_EQ(0, browser()->GetTabStripModel()->active_index());
    EXPECT_EQ(focus_manager->GetFocusedView(),
              contents_container_views[0]->contents_view());

    focus_manager->AdvanceFocus(true);
    EXPECT_EQ(0, browser()->GetTabStripModel()->active_index());
    EXPECT_EQ(start_view, focus_manager->GetFocusedView());
  }
};

IN_PROC_BROWSER_TEST_F(BrowserViewFocusTest, BrowsersRememberFocus) {
  ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
  ASSERT_TRUE(embedded_test_server()->Start());

  // First we navigate to our test page.
  GURL url = embedded_test_server()->GetURL(kSimplePage);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  gfx::NativeWindow window = browser()->GetWindow()->GetNativeWindow();

  // The focus should be on the Tab contents.
  ASSERT_TRUE(IsViewFocused(VIEW_ID_TAB_CONTAINER));
  // Now hide the window, show it again, the focus should not have changed.
  ui_test_utils::HideNativeWindow(window);
  ASSERT_TRUE(ui_test_utils::ShowAndFocusNativeWindow(window));
  ASSERT_TRUE(IsViewFocused(VIEW_ID_TAB_CONTAINER));

  chrome::FocusLocationBar(browser());
  ASSERT_TRUE(IsViewFocused(VIEW_ID_OMNIBOX));
  // Hide the window, show it again, the focus should not have changed.
  ui_test_utils::HideNativeWindow(window);
  ASSERT_TRUE(ui_test_utils::ShowAndFocusNativeWindow(window));
  ASSERT_TRUE(IsViewFocused(VIEW_ID_OMNIBOX));

  // The rest of this test does not make sense on Linux because the behavior
  // of Activate() is not well defined and can vary by window manager.
#if BUILDFLAG(IS_WIN)
  // Open a new browser window.
  BrowserWindowInterface* browser2 = CreateBrowserWindow(
      BrowserWindowCreateParams(browser()->GetProfile(),
                                /*from_user_gesture=*/true));
  ASSERT_TRUE(browser2);
  chrome::AddTabAt(browser2, GURL(), -1, true);
  browser2->GetWindow()->Show();
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser2, url));

  gfx::NativeWindow window2 = browser2->GetWindow()->GetNativeWindow();
  BrowserView* browser_view2 = BrowserView::GetBrowserViewForBrowser(browser2);
  ASSERT_TRUE(browser_view2);
  const views::Widget* widget2 =
      views::Widget::GetWidgetForNativeWindow(window2);
  ASSERT_TRUE(widget2);
  const views::FocusManager* focus_manager2 = widget2->GetFocusManager();
  ASSERT_TRUE(focus_manager2);
  EXPECT_EQ(browser_view2->contents_web_view(),
            focus_manager2->GetFocusedView());

  // Switch to the 1st browser window, focus should still be on the location
  // bar and the second browser should have nothing focused.
  browser()->GetWindow()->Activate();
  ASSERT_TRUE(IsViewFocused(VIEW_ID_OMNIBOX));
  EXPECT_EQ(nullptr, focus_manager2->GetFocusedView());

  // Switch back to the second browser, focus should still be on the page.
  browser2->GetWindow()->Activate();
  views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window);
  ASSERT_TRUE(widget);
  EXPECT_EQ(nullptr, widget->GetFocusManager()->GetFocusedView());
  EXPECT_EQ(browser_view2->contents_web_view(),
            focus_manager2->GetFocusedView());

  // Close the 2nd browser to avoid a DCHECK().
  browser_view2->Close();
#endif
}

// Helper class that tracks view classes receiving focus.
class FocusedViewClassRecorder : public views::FocusChangeListener {
 public:
  explicit FocusedViewClassRecorder(views::FocusManager* focus_manager)
      : focus_manager_(focus_manager) {
    focus_manager_->AddFocusChangeListener(this);
  }

  FocusedViewClassRecorder(const FocusedViewClassRecorder&) = delete;
  FocusedViewClassRecorder& operator=(const FocusedViewClassRecorder&) = delete;
  ~FocusedViewClassRecorder() override {
    focus_manager_->RemoveFocusChangeListener(this);
  }

  bool GetHasFocusedOnNonWebView() { return has_focused_on_non_webview_; }
  int GetFocusChangeCount() { return focus_change_count; }

 private:
  // Inherited from views::FocusChangeListener
  void OnDidChangeFocus(views::View* focused_before,
                        views::View* focused_now) override {
    if (focused_now) {
      if (!views::IsViewClass<views::WebView>(focused_now)) {
        // Focused views could be destroyed. Track what we want to test for when
        // OnDidChangeFocus is called.
        has_focused_on_non_webview_ = true;
      }
    }
    focus_change_count++;
  }

  raw_ptr<views::FocusManager> focus_manager_;
  bool has_focused_on_non_webview_ = false;
  int focus_change_count = 0;
};

// Switching tabs does not focus views unexpectedly.
// (bug http://crbug.com/41359257, bug http://crbug.com/41351034)
IN_PROC_BROWSER_TEST_F(BrowserViewFocusTest, TabChangesAvoidSpuriousFocus) {
  ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
  ASSERT_TRUE(embedded_test_server()->Start());

  // First we navigate to our test page.
  GURL url = embedded_test_server()->GetURL(kSimplePage);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));

  // Create another tab.
  ASSERT_TRUE(AddTabAtIndex(1, url, ui::PAGE_TRANSITION_TYPED));

  // Begin recording focus changes.
  gfx::NativeWindow window = browser()->GetWindow()->GetNativeWindow();
  views::Widget* widget = views::Widget::GetWidgetForNativeWindow(window);
  views::FocusManager* focus_manager = widget->GetFocusManager();
  FocusedViewClassRecorder focus_change_recorder(focus_manager);

  // Switch tabs using ctrl+tab.
  ASSERT_TRUE(ui_test_utils::SendKeyPressSync(browser(), ui::VKEY_TAB, true,
                                              false, false, false));

  // Everything that was focused on must be a WebView.
  EXPECT_FALSE(focus_change_recorder.GetHasFocusedOnNonWebView());
  EXPECT_EQ(focus_change_recorder.GetFocusChangeCount(), 2);
}

IN_PROC_BROWSER_TEST_F(BrowserViewFocusTest, FocusOrder) {
  ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
  ASSERT_TRUE(embedded_test_server()->Start());

  // Create a split tab.
  GURL url = GURL(url::kAboutBlankURL);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ASSERT_TRUE(AddTabAtIndex(1, url, ui::PAGE_TRANSITION_TYPED));
  browser()->GetTabStripModel()->AddToNewSplit(
      {0}, split_tabs::SplitTabVisualData(),
      split_tabs::SplitTabCreatedSource::kToolbarButton);
  ASSERT_EQ(1, browser()->GetTabStripModel()->active_index());

  TestSplitTabFocusOrder();
}

// Tests that when we activate and then close the right tab in a split, focus
// order is preserved. Previously there was a bug where this would cause the
// left/right containers' order in the view hierarchy to be reversed.
IN_PROC_BROWSER_TEST_F(BrowserViewFocusTest, FocusOrderAfterClosingRightTab) {
  ASSERT_TRUE(ui_test_utils::BringBrowserWindowToFront(browser()));
  ASSERT_TRUE(embedded_test_server()->Start());

  // Create a split tab.
  GURL url = GURL(url::kAboutBlankURL);
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ASSERT_TRUE(AddTabAtIndex(1, url, ui::PAGE_TRANSITION_TYPED));
  browser()->GetTabStripModel()->AddToNewSplit(
      {0}, split_tabs::SplitTabVisualData(),
      split_tabs::SplitTabCreatedSource::kToolbarButton);
  ASSERT_EQ(1, browser()->GetTabStripModel()->active_index());

  // Close the right tab, then create another split.
  browser()->GetTabStripModel()->CloseWebContentsAt(1,
                                                    TabCloseTypes::CLOSE_NONE);
  ASSERT_EQ(1, browser()->GetTabStripModel()->count());
  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), url));
  ASSERT_TRUE(AddTabAtIndex(1, url, ui::PAGE_TRANSITION_TYPED));
  browser()->GetTabStripModel()->AddToNewSplit(
      {0}, split_tabs::SplitTabVisualData(),
      split_tabs::SplitTabCreatedSource::kToolbarButton);
  ASSERT_EQ(1, browser()->GetTabStripModel()->active_index());

  TestSplitTabFocusOrder();
}
