// Copyright 2019 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/cocoa/tab_menu_bridge.h"

#import <Cocoa/Cocoa.h>

#include <memory>

#include "base/functional/callback_helpers.h"
#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/test/task_environment.h"
#include "chrome/browser/favicon/favicon_utils.h"
#include "chrome/browser/ui/tab_ui_helper.h"
#include "chrome/browser/ui/tabs/public/tab_features.h"
#include "chrome/browser/ui/tabs/tab_enums.h"
#include "chrome/browser/ui/tabs/tab_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/tabs/test_tab_strip_model_delegate.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/test/base/testing_profile.h"
#include "components/tabs/public/tab_interface.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/web_contents_tester.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"

namespace {
constexpr int kStaticItemCount = 4;

class TabStripModelUiHelperDelegate : public TestTabStripModelDelegate {
 public:
  void WillAddWebContents(content::WebContents* contents) override {
    TestTabStripModelDelegate::WillAddWebContents(contents);

    favicon::CreateContentFaviconDriverForWebContents(contents);
  }
};

}  // namespace

class TabMenuBridgeTest : public ::testing::Test {
 public:
  void SetUp() override {
    profile_ = std::make_unique<TestingProfile>();
    rvh_test_enabler_ = std::make_unique<content::RenderViewHostTestEnabler>();
    delegate_ = std::make_unique<TabStripModelUiHelperDelegate>();
    model_ = std::make_unique<TabStripModel>(delegate_.get(), profile_.get());
    menu_root_ = ItemWithTitle(@"Tab");
    menu_ = [[NSMenu alloc] initWithTitle:@"Tab"];
    menu_root_.submenu = menu_;

    AddStaticItems(menu_);
  }

  void TearDown() override { model_->CloseAllTabs(); }

  NSMenuItem* menu_root() { return menu_root_; }
  NSMenu* menu() { return menu_; }
  TabStripModel* model() { return model_.get(); }
  TabStripModelDelegate* delegate() { return delegate_.get(); }

  void AddStaticItems(NSMenu* menu) {
    [menu_ addItem:ItemWithTitle(@"Static 1")];
    [menu_ addItem:ItemWithTitle(@"Static 2")];
    [menu_ addItem:ItemWithTitle(@"Static 3")];
    [menu_ addItem:SeparatorItem()];
  }

  std::unique_ptr<content::WebContents> CreateWebContents(
      const std::string& title) {
    std::unique_ptr<content::WebContents> contents =
        content::WebContentsTester::CreateTestWebContents(profile_.get(),
                                                          nullptr);
    content::WebContentsTester::For(contents.get())
        ->SetTitle(base::UTF8ToUTF16(title));
    return contents;
  }

  content::WebContents* GetModelWebContentsAt(int index) {
    return model()->GetWebContentsAt(index);
  }

  void AddModelTabNamed(const std::string& name,
                        TabStripModel* tab_strip_model) {
    std::unique_ptr<tabs::TabModel> tab_model =
        std::make_unique<tabs::TabModel>(CreateWebContents(name),
                                         tab_strip_model);
    tabs::TabFeatures* const tab_features = tab_model->GetTabFeatures();

    std::unique_ptr<TabUIHelper> tab_ui_helper =
        tabs::TabFeatures::GetUserDataFactoryForTesting()
            .CreateInstance<TabUIHelper>(*tab_model, *tab_model);

    tab_features->SetTabUIHelperForTesting(std::move(tab_ui_helper));
    tab_strip_model->AppendTab(std::move(tab_model), true);
  }

  int ModelIndexForTabNamed(const std::string& name) {
    std::u16string title16 = base::UTF8ToUTF16(name);
    for (int i = 0; i < model()->count(); ++i) {
      if (model()->GetWebContentsAt(i)->GetTitle() == title16) {
        return i;
      }
    }
    return -1;
  }

  void RemoveModelTabNamed(const std::string& name) {
    int index = ModelIndexForTabNamed(name);
    DCHECK(index >= 0);
    model()->CloseWebContentsAt(index, TabCloseTypes::CLOSE_NONE);
  }

  void RenameModelTabNamed(const std::string& old_name,
                           const std::string& new_name) {
    int index = ModelIndexForTabNamed(old_name);
    if (index >= 0) {
      content::WebContents* contents = model()->GetWebContentsAt(index);
      content::WebContentsTester::For(contents)->SetTitle(
          base::UTF8ToUTF16(new_name));
      // The way WebContentsTester updates the title avoids the usual
      // notification mechanism for TabStripModel, so manually synthesize the
      // update notification here.
      model()->UpdateWebContentsState(contents, TabChangeType::kAll);
    }
  }

  void ReplaceModelTabNamed(const std::string& old_name,
                            const std::string& new_name) {
    int index = ModelIndexForTabNamed(old_name);
    if (index >= 0) {
      std::unique_ptr<content::WebContents> old_contents =
          model()->DiscardWebContents(model()->GetWebContentsAt(index),
                                      CreateWebContents(new_name));
      // Let the old WebContents be destroyed here.
    }
  }

  void ActivateModelTabNamed(const std::string& name) {
    int index = ModelIndexForTabNamed(name);
    DCHECK(index >= 0);
    model()->ActivateTabAt(index);
  }

  void AddModelTabToGroup(const std::vector<int>& tab_indices) {
    model()->AddToNewGroup(tab_indices);
  }

  void RemoveModelTabFromGroup(const std::vector<int>& tab_indices) {
    model()->RemoveFromGroup(tab_indices);
  }

  NSMenuItem* MenuItemForTabNamed(const std::string& name) {
    return [menu() itemWithTitle:base::SysUTF8ToNSString(name)];
  }

  void ExpectDynamicTabsInMenuAre(const std::vector<std::string>& titles) {
    std::vector<std::string> actual_titles;
    for (int i = kStaticItemCount; i < menu().numberOfItems; ++i) {
      actual_titles.push_back(
          base::SysNSStringToUTF8([menu() itemAtIndex:i].title));
    }

    ASSERT_EQ(actual_titles.size(), titles.size());
    for (int i = 0; i < static_cast<int>(titles.size()); ++i) {
      EXPECT_EQ(actual_titles[i], titles[i]);
    }
  }

  std::string ActiveTabName() {
    return base::UTF16ToUTF8(model()->GetActiveWebContents()->GetTitle());
  }

  void ExpectActiveMenuItemNameIs(const std::string& name) {
    std::vector<std::string> active_items;
    // Check the static items too, to make sure none of them are checked.
    for (int i = 0; i < menu().numberOfItems; ++i) {
      NSMenuItem* item = [menu() itemAtIndex:i];
      if (item.state == NSControlStateValueOn) {
        active_items.push_back(base::SysNSStringToUTF8(item.title));
      }
    }

    ASSERT_EQ(active_items.size(), 1u);
    EXPECT_EQ(name, active_items[0]);
  }

  NSMenuItem* GetActiveMenuItem() {
    for (int i = 0; i < menu().numberOfItems; ++i) {
      NSMenuItem* item = [menu() itemAtIndex:i];
      if (item.state == NSControlStateValueOn) {
        return item;
      }
    }

    return nullptr;
  }

  TestingProfile* profile() { return profile_.get(); }

 private:
  NSMenuItem* ItemWithTitle(NSString* title) {
    return [[NSMenuItem alloc] initWithTitle:title
                                      action:nil
                               keyEquivalent:@""];
  }

  NSMenuItem* SeparatorItem() { return [NSMenuItem separatorItem]; }

  content::BrowserTaskEnvironment task_environment_;

  std::unique_ptr<TestingProfile> profile_;
  std::unique_ptr<content::RenderViewHostTestEnabler> rvh_test_enabler_;
  std::unique_ptr<TabStripModelUiHelperDelegate> delegate_;
  std::unique_ptr<TabStripModel> model_;
  NSMenuItem* __strong menu_root_;
  NSMenu* __strong menu_;
  tabs::TabModel::PreventFeatureInitializationForTesting prevent_;
};

TEST_F(TabMenuBridgeTest, CreatesBlankMenu) {
  TabMenuBridge bridge(menu_root());
  bridge.SetTabStripModel(model());
  bridge.SetForceRebuildMenuForTesting(true);
  EXPECT_EQ(menu().numberOfItems, kStaticItemCount);
  ExpectDynamicTabsInMenuAre({});
}

TEST_F(TabMenuBridgeTest, TracksModelUpdates) {
  TabStripModel* const tab_strip_model = model();
  TabMenuBridge bridge(menu_root());
  bridge.SetForceRebuildMenuForTesting(true);
  bridge.SetTabStripModel(tab_strip_model);

  AddModelTabNamed("Tab 1", tab_strip_model);
  AddModelTabNamed("Tab 2", tab_strip_model);
  AddModelTabNamed("Tab 3", tab_strip_model);
  ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 2", "Tab 3"});

  RemoveModelTabNamed("Tab 2");
  ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 3"});

  AddModelTabNamed("Tab 2", tab_strip_model);
  ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 3", "Tab 2"});

  RenameModelTabNamed("Tab 1", "Tab 4");
  ExpectDynamicTabsInMenuAre({"Tab 4", "Tab 3", "Tab 2"});

  content::WebContents* old_contents = GetModelWebContentsAt(0);
  ReplaceModelTabNamed("Tab 4", "Tab 5");
  ASSERT_NE(GetModelWebContentsAt(0), old_contents);
  ExpectDynamicTabsInMenuAre({"Tab 5", "Tab 3", "Tab 2"});
}

// Tests that dynamic menu items added by the bridge are removed on
// bridge destruction.
TEST_F(TabMenuBridgeTest, RemoveDynamicMenuItemsOnDestruct) {
  TabStripModel* const tab_strip_model = model();
  std::unique_ptr<TabMenuBridge> bridge =
      std::make_unique<TabMenuBridge>(menu_root());
  bridge->SetForceRebuildMenuForTesting(true);
  bridge->SetTabStripModel(tab_strip_model);

  AddModelTabNamed("Tab 1", tab_strip_model);
  AddModelTabNamed("Tab 2", tab_strip_model);
  AddModelTabNamed("Tab 3", tab_strip_model);
  ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 2", "Tab 3"});

  bridge.reset();

  ExpectDynamicTabsInMenuAre({});
}

TEST_F(TabMenuBridgeTest, ClickingMenuActivatesTab) {
  TabStripModel* const tab_strip_model = model();
  TabMenuBridge bridge(menu_root());
  bridge.SetForceRebuildMenuForTesting(true);
  bridge.SetTabStripModel(tab_strip_model);

  AddModelTabNamed("Tab 1", tab_strip_model);
  AddModelTabNamed("Tab 2", tab_strip_model);
  EXPECT_EQ(ActiveTabName(), "Tab 2");

  NSMenuItem* tab1_item = MenuItemForTabNamed("Tab 1");

  // Don't go through NSMenuItem's normal click-dispatching machinery here - it
  // would add flake potential to this test without improving coverage. Instead,
  // call straight through to the C++-side callback:
  bridge.OnDynamicItemChosen(tab1_item);
  EXPECT_EQ(ActiveTabName(), "Tab 1");

  // Activation does not re-order the menu.
  ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 2"});
}

// This is a regression test for a bug found during development.
TEST_F(TabMenuBridgeTest, SwappingBridgeRecreatesMenu) {
  TabStripModel* const tab_strip_model = model();
  auto bridge = std::make_unique<TabMenuBridge>(menu_root());
  bridge->SetForceRebuildMenuForTesting(true);
  bridge->SetTabStripModel(tab_strip_model);

  AddModelTabNamed("Tab 1", tab_strip_model);

  auto model2 = std::make_unique<TabStripModel>(delegate(), profile());
  AddModelTabNamed("Tab 2", model2.get());

  bridge->SetTabStripModel(model2.get());
  ExpectDynamicTabsInMenuAre({"Tab 2"});

  // Simulate one of the tabs in the model being updated - if the computed
  // indexes are wrong, this call will DCHECK.
  model2->UpdateWebContentsState(model2->GetWebContentsAt(0),
                                 TabChangeType::kAll);

  model2->CloseAllTabs();

  // model2 gets torn down before bridge here, which exercises the code in
  // TabMenuBridge for handling the TabStripModel being torn down (eg via
  // browser window close) while the TabMenuBridge still exists. If that code
  // does not correctly forget about the TabStripModel, this test will crash
  // here in ASAN builds.
}

TEST_F(TabMenuBridgeTest, ActiveItemTracksChanges) {
  TabStripModel* const tab_strip_model = model();
  TabMenuBridge bridge(menu_root());
  bridge.SetForceRebuildMenuForTesting(true);
  bridge.SetTabStripModel(tab_strip_model);

  AddModelTabNamed("Tab 1", tab_strip_model);
  AddModelTabNamed("Tab 2", tab_strip_model);
  AddModelTabNamed("Tab 3", tab_strip_model);
  ExpectActiveMenuItemNameIs("Tab 3");

  ActivateModelTabNamed("Tab 2");
  ExpectActiveMenuItemNameIs("Tab 2");

  ActivateModelTabNamed("Tab 3");
  ExpectActiveMenuItemNameIs("Tab 3");

  RemoveModelTabNamed("Tab 1");
  ExpectActiveMenuItemNameIs("Tab 3");
}


// Regression test: clicking a stale menu item after a tab has been closed
// should not crash.
TEST_F(TabMenuBridgeTest, ClickingStaleMenuItemDoesNotCrash) {
  TabStripModel* const tab_strip_model = model();
  TabMenuBridge bridge(menu_root());
  bridge.SetForceRebuildMenuForTesting(true);
  bridge.SetTabStripModel(tab_strip_model);

  AddModelTabNamed("Tab 1", tab_strip_model);
  AddModelTabNamed("Tab 2", tab_strip_model);
  AddModelTabNamed("Tab 3", tab_strip_model);
  ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 2", "Tab 3"});

  // Save a reference to the last menu item (corresponding to "Tab 3").
  NSMenuItem* tab3_item = MenuItemForTabNamed("Tab 3");

  // Close "Tab 3" - this rebuilds the menu, removing tab3_item.
  RemoveModelTabNamed("Tab 3");
  ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 2"});

  // Simulate the user clicking the now-stale menu item. This should not crash
  // and should not change the active tab.
  bridge.OnDynamicItemChosen(tab3_item);

  // The active tab should not have changed.
  EXPECT_EQ(ActiveTabName(), "Tab 2");
}

// Test that clicking a menu item activates the correct tab even if tabs have
// been reordered since the menu was built.
TEST_F(TabMenuBridgeTest, ClickingMenuItemAfterReorderActivatesCorrectTab) {
  TabStripModel* const tab_strip_model = model();
  TabMenuBridge bridge(menu_root());
  bridge.SetForceRebuildMenuForTesting(true);
  bridge.SetTabStripModel(tab_strip_model);

  AddModelTabNamed("Tab 1", tab_strip_model);
  AddModelTabNamed("Tab 2", tab_strip_model);
  AddModelTabNamed("Tab 3", tab_strip_model);
  ExpectDynamicTabsInMenuAre({"Tab 1", "Tab 2", "Tab 3"});

  // Save a reference to the menu item for "Tab 1" (at model index 0).
  NSMenuItem* tab1_item = MenuItemForTabNamed("Tab 1");

  // Stop forcing rebuilds so the move makes the menu stale instead of
  // rebuilding it (which would recycle NSMenuItems to different tabs).
  bridge.SetForceRebuildMenuForTesting(false);

  // Move "Tab 1" to the end (index 2). The menu items are now stale.
  tab_strip_model->MoveWebContentsAt(0, 2, false);

  // Clicking the saved menu item should still activate "Tab 1", not whatever
  // is now at its old position.
  bridge.OnDynamicItemChosen(tab1_item);
  EXPECT_EQ(ActiveTabName(), "Tab 1");
}
