// Copyright 2024 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/webui/webui_embedding_context.h"

#include "base/test/mock_callback.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/resource_coordinator/tab_lifecycle_unit_external.h"
#include "chrome/browser/resource_coordinator/tab_lifecycle_unit_source.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "content/public/test/browser_test.h"

using WebUIEmbeddingContextTest = InProcessBrowserTest;

namespace webui {

IN_PROC_BROWSER_TEST_F(
    WebUIEmbeddingContextTest,
    InitEmbeddingContext_MovingTabsAcrossWindowsUpdatesContext) {
  // Create a browser with 2 tabs.
  content::WebContents* tab_contents =
      chrome::AddAndReturnTabAt(browser(), GURL(url::kAboutBlankURL), 1, true);
  tabs::TabInterface* tab_interface =
      browser()->tab_strip_model()->GetTabAtIndex(1);
  EXPECT_EQ(2, browser()->tab_strip_model()->count());
  EXPECT_EQ(tab_interface, GetTabInterface(tab_contents));
  EXPECT_EQ(browser(), webui::GetBrowserWindowInterface(tab_contents));

  base::MockCallback<base::RepeatingClosure> tab_changed_callback;
  base::MockCallback<base::RepeatingClosure> browser_changed_callback;
  base::CallbackListSubscription tab_subscription =
      RegisterTabInterfaceChanged(tab_contents, tab_changed_callback.Get());
  base::CallbackListSubscription browser_subscription =
      RegisterBrowserWindowInterfaceChanged(tab_contents,
                                            browser_changed_callback.Get());

  // Move the tab into a new browser window.
  EXPECT_CALL(tab_changed_callback, Run).Times(0);
  EXPECT_CALL(browser_changed_callback, Run).Times(1);
  ui_test_utils::BrowserCreatedObserver browser_created_observer;
  chrome::MoveTabsToNewWindow(browser(), {1});
  BrowserWindowInterface* new_browser = browser_created_observer.Wait();
  ASSERT_TRUE(new_browser);
  EXPECT_EQ(1, browser()->tab_strip_model()->count());
  EXPECT_EQ(1, new_browser->tab_strip_model()->count());
  EXPECT_EQ(tab_interface, GetTabInterface(tab_contents));
  EXPECT_EQ(new_browser, webui::GetBrowserWindowInterface(tab_contents));
  testing::Mock::VerifyAndClearExpectations(&tab_changed_callback);
  testing::Mock::VerifyAndClearExpectations(&browser_changed_callback);

  // Move the tab back into its original browser window. This results in the
  // closing and destruction of the previously created browser. Only one
  // browser-changed notification is expected as the tab is moved to the new
  // browser before the old browser is destroyed.
  EXPECT_CALL(tab_changed_callback, Run).Times(0);
  EXPECT_CALL(browser_changed_callback, Run).Times(1);
  chrome::MoveTabsToExistingWindow(new_browser, browser(), {0});
  EXPECT_EQ(2, browser()->tab_strip_model()->count());
  EXPECT_EQ(tab_interface, GetTabInterface(tab_contents));
  EXPECT_EQ(browser(), webui::GetBrowserWindowInterface(tab_contents));
  testing::Mock::VerifyAndClearExpectations(&browser_changed_callback);
}

IN_PROC_BROWSER_TEST_F(WebUIEmbeddingContextTest,
                       InitEmbeddingContext_SetCorrectlyOnTabDiscard) {
  // Create a browser with 2 tabs.
  content::WebContents* tab_contents =
      chrome::AddAndReturnTabAt(browser(), GURL(url::kAboutBlankURL), 1, false);
  tabs::TabInterface* tab_interface =
      browser()->tab_strip_model()->GetTabAtIndex(1);
  EXPECT_EQ(2, browser()->tab_strip_model()->count());
  EXPECT_EQ(tab_interface, GetTabInterface(tab_contents));
  EXPECT_EQ(browser(), webui::GetBrowserWindowInterface(tab_contents));

  // Discard the tab.
  EXPECT_NE(browser()->tab_strip_model()->GetActiveTab(), tab_interface);
  auto* lifecycle_unit =
      resource_coordinator::TabLifecycleUnitSource::GetTabLifecycleUnitExternal(
          tab_contents);
  lifecycle_unit->DiscardTab(mojom::LifecycleUnitDiscardReason::URGENT);

  EXPECT_EQ(mojom::LifecycleUnitState::DISCARDED,
            lifecycle_unit->GetTabState());
  tab_contents = browser()->tab_strip_model()->GetTabAtIndex(1)->GetContents();

  // The tab and browser interfaces should remain associated with the tab
  // contents after discard.
  EXPECT_EQ(tab_interface, GetTabInterface(tab_contents));
  EXPECT_EQ(browser(), webui::GetBrowserWindowInterface(tab_contents));
}

IN_PROC_BROWSER_TEST_F(WebUIEmbeddingContextTest,
                       SetTabInterface_TracksEmbedderStateCorrectly) {
  // Create a new WebContents, the tab and browser should start empty.
  std::unique_ptr<content::WebContents> host_contents =
      content::WebContents::Create(
          content::WebContents::CreateParams(browser()->GetProfile()));
  EXPECT_FALSE(GetTabInterface(host_contents.get()));
  EXPECT_FALSE(webui::GetBrowserWindowInterface(host_contents.get()));

  base::MockCallback<base::RepeatingClosure> tab_changed_callback;
  base::MockCallback<base::RepeatingClosure> browser_changed_callback;
  base::CallbackListSubscription tab_subscription = RegisterTabInterfaceChanged(
      host_contents.get(), tab_changed_callback.Get());
  base::CallbackListSubscription browser_subscription =
      RegisterBrowserWindowInterfaceChanged(host_contents.get(),
                                            browser_changed_callback.Get());

  // Set the tab interface, the tracked state should update.
  EXPECT_CALL(tab_changed_callback, Run).Times(1);
  EXPECT_CALL(browser_changed_callback, Run).Times(1);
  tabs::TabInterface* tab_interface =
      browser()->tab_strip_model()->GetActiveTab();
  SetTabInterface(host_contents.get(), tab_interface);
  EXPECT_EQ(tab_interface, GetTabInterface(host_contents.get()));
  EXPECT_EQ(browser(), webui::GetBrowserWindowInterface(host_contents.get()));
  testing::Mock::VerifyAndClearExpectations(&tab_changed_callback);
  testing::Mock::VerifyAndClearExpectations(&browser_changed_callback);

  // Reset the tab interface, this should be reflected in the tracked state.
  EXPECT_CALL(tab_changed_callback, Run).Times(1);
  EXPECT_CALL(browser_changed_callback, Run).Times(1);
  SetTabInterface(host_contents.get(), nullptr);
  EXPECT_FALSE(GetTabInterface(host_contents.get()));
  EXPECT_FALSE(webui::GetBrowserWindowInterface(host_contents.get()));
}

IN_PROC_BROWSER_TEST_F(WebUIEmbeddingContextTest,
                       SetTabInterface_NotifiesBrowserChanges) {
  // Create a new WebContents and set the emebdding tab interface.
  std::unique_ptr<content::WebContents> host_contents =
      content::WebContents::Create(
          content::WebContents::CreateParams(browser()->GetProfile()));
  chrome::AddTabAt(browser(), GURL(url::kAboutBlankURL), 1, true);
  tabs::TabInterface* tab_interface =
      browser()->tab_strip_model()->GetTabAtIndex(1);
  SetTabInterface(host_contents.get(), tab_interface);
  EXPECT_EQ(tab_interface, GetTabInterface(host_contents.get()));
  EXPECT_EQ(browser(), webui::GetBrowserWindowInterface(host_contents.get()));

  base::MockCallback<base::RepeatingClosure> tab_changed_callback;
  base::MockCallback<base::RepeatingClosure> browser_changed_callback;
  base::CallbackListSubscription tab_subscription = RegisterTabInterfaceChanged(
      host_contents.get(), tab_changed_callback.Get());
  base::CallbackListSubscription browser_subscription =
      RegisterBrowserWindowInterfaceChanged(host_contents.get(),
                                            browser_changed_callback.Get());

  // Create a new browser and move the tab over, the appropriate browser changed
  // notification should be fired.
  EXPECT_CALL(tab_changed_callback, Run).Times(0);
  EXPECT_CALL(browser_changed_callback, Run).Times(1);

  BrowserWindowInterface* dst_browser = CreateBrowser(browser()->GetProfile());
  std::unique_ptr<tabs::TabModel> detached_tab =
      browser()->tab_strip_model()->DetachTabAtForInsertion(1);
  EXPECT_EQ(tab_interface, detached_tab.get());
  dst_browser->tab_strip_model()->InsertDetachedTabAt(
      1, std::move(detached_tab), AddTabTypes::ADD_NONE);

  EXPECT_EQ(tab_interface, GetTabInterface(host_contents.get()));
  EXPECT_EQ(dst_browser, webui::GetBrowserWindowInterface(host_contents.get()));
}

IN_PROC_BROWSER_TEST_F(WebUIEmbeddingContextTest,
                       SetTabInterface_TrackedStateResetOnEmbedderDestruction) {
  // Create a new WebContents and set the emebdding tab interface.
  std::unique_ptr<content::WebContents> host_contents =
      content::WebContents::Create(
          content::WebContents::CreateParams(browser()->GetProfile()));
  chrome::AddTabAt(browser(), GURL(url::kAboutBlankURL), 1, true);
  tabs::TabInterface* tab_interface =
      browser()->tab_strip_model()->GetTabAtIndex(1);
  SetTabInterface(host_contents.get(), tab_interface);
  EXPECT_EQ(tab_interface, GetTabInterface(host_contents.get()));
  EXPECT_EQ(browser(), webui::GetBrowserWindowInterface(host_contents.get()));

  base::MockCallback<base::RepeatingClosure> tab_changed_callback;
  base::MockCallback<base::RepeatingClosure> browser_changed_callback;
  base::CallbackListSubscription tab_subscription = RegisterTabInterfaceChanged(
      host_contents.get(), tab_changed_callback.Get());
  base::CallbackListSubscription browser_subscription =
      RegisterBrowserWindowInterfaceChanged(host_contents.get(),
                                            browser_changed_callback.Get());

  // Destroy the embedding tab, this should be reflected in tracked state.
  EXPECT_CALL(tab_changed_callback, Run).Times(1);
  EXPECT_CALL(browser_changed_callback, Run).Times(1);
  browser()->tab_strip_model()->DetachAndDeleteWebContentsAt(1);
  EXPECT_FALSE(GetTabInterface(host_contents.get()));
  EXPECT_FALSE(webui::GetBrowserWindowInterface(host_contents.get()));
}

}  // namespace webui
