// Copyright 2025 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_group_menu_bridge.h"

#import <Cocoa/Cocoa.h>

#include <memory>

#include "base/strings/sys_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/uuid.h"
#include "chrome/browser/tab_group_sync/tab_group_sync_service_factory.h"
#include "chrome/browser/ui/toolbar/app_menu_model.h"
#include "chrome/grit/generated_resources.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "components/saved_tab_groups/public/features.h"
#include "components/saved_tab_groups/public/saved_tab_group.h"
#include "components/saved_tab_groups/public/tab_group_sync_service.h"
#include "components/saved_tab_groups/test_support/fake_tab_group_sync_service.h"
#include "components/tab_groups/tab_group_color.h"
#include "content/public/browser/web_contents.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/gtest_mac.h"
#include "ui/base/l10n/l10n_util.h"
#include "url/gurl.h"

class TabGroupMenuBridgeTest : public BrowserWithTestWindowTest {
 public:

  void SetUp() override {
    BrowserWithTestWindowTest::SetUp();
    service_ = std::make_unique<tab_groups::FakeTabGroupSyncService>();

    // Create a dummy main menu.
    main_menu_ = [[NSMenu alloc] init];
    NSMenuItem* app_menu_item = [[NSMenuItem alloc] init];
    [main_menu_ addItem:app_menu_item];
    [NSApp setMainMenu:main_menu_];

    // Create the "Saved Tab Groups" menu that the bridge will use.
    tab_groups_menu_root_ = [[NSMenuItem alloc] initWithTitle:@"Tab Groups"
                                                       action:nil
                                                keyEquivalent:@""];
    tab_groups_menu_root_.tag = AppMenuModel::kSavedTabGroupsMenuPlaceholder;
    tab_groups_menu_ = [[NSMenu alloc] initWithTitle:@"Tab Groups"];
    tab_groups_menu_root_.submenu = tab_groups_menu_;
    [main_menu_ addItem:tab_groups_menu_root_];

    // Add the static "New Tab Group" item.
    NSMenuItem* new_group_item =
        [[NSMenuItem alloc] initWithTitle:@"New Tab Group"
                                   action:nil
                            keyEquivalent:@""];
    new_group_item.tag = IDC_CREATE_NEW_TAB_GROUP;
    [tab_groups_menu_ addItem:new_group_item];
  }

  void TearDown() override {
    [NSApp setMainMenu:nil];
    BrowserWithTestWindowTest::TearDown();
  }

 protected:
  NSMenu* menu() { return tab_groups_menu_; }
  tab_groups::TabGroupSyncService* service() { return service_.get(); }

  // Helper to add a group to the service.
  void AddGroup(const std::u16string& title,
                const tab_groups::TabGroupColorId& color,
                const std::vector<GURL>& urls) {
    tab_groups::SavedTabGroup group(title, color, {}, std::nullopt);
    for (const auto& url : urls) {
      tab_groups::SavedTabGroupTab tab(url, u"Tab Title", group.saved_guid(),
                                       /*position=*/std::nullopt);
      group.AddTabLocally(std::move(tab));
    }
    service()->AddGroup(std::move(group));
  }

  void ExpectGroupTitlesInMenu(const std::vector<std::string>& titles) {
    std::vector<std::string> actual_titles;
    bool found_separator = false;
    for (NSMenuItem* item in [menu() itemArray]) {
      if (item.tag == IDC_CREATE_NEW_TAB_GROUP) {
        continue;
      }
      if ([item isSeparatorItem]) {
        found_separator = true;
        continue;
      }
      if (item.hasSubmenu) {
        actual_titles.push_back(base::SysNSStringToUTF8(item.title));
      }
    }

    EXPECT_EQ(!titles.empty(), found_separator);

    std::sort(actual_titles.begin(), actual_titles.end());
    std::vector<std::string> sorted_titles = titles;
    std::sort(sorted_titles.begin(), sorted_titles.end());

    ASSERT_EQ(sorted_titles.size(), actual_titles.size());
    for (size_t i = 0; i < sorted_titles.size(); ++i) {
      EXPECT_EQ(sorted_titles[i], actual_titles[i]);
    }
  }

  std::unique_ptr<tab_groups::TabGroupSyncService> service_;
  NSMenu* __strong main_menu_;
  NSMenuItem* __strong tab_groups_menu_root_;
  NSMenu* __strong tab_groups_menu_;
};

// Regression test for crbug.com/482257280.
TEST_F(TabGroupMenuBridgeTest, BuildMenuDuringConstructionWithExistingGroups) {
  // Add a group before constructing the bridge so OnInitialized() calls
  // BuildMenu() during construction with groups present.
  AddGroup(u"Pre-existing Group", tab_groups::TabGroupColorId::kBlue,
           {GURL("https://example.com")});

  // Must not crash obtaining a color provider.
  TabGroupMenuBridge bridge(profile(), service());

  ExpectGroupTitlesInMenu({"Pre-existing Group"});
}

TEST_F(TabGroupMenuBridgeTest, CreatesBlankMenu) {
  TabGroupMenuBridge bridge(profile(), service());
  bridge.BuildMenu();
  // Only the static "New Tab Group" item should be present.
  EXPECT_EQ(1, menu().numberOfItems);
  ExpectGroupTitlesInMenu({});
}

TEST_F(TabGroupMenuBridgeTest, TracksGroupUpdates) {
  TabGroupMenuBridge bridge(profile(), service());
  bridge.BuildMenu();

  AddGroup(u"Group 1", tab_groups::TabGroupColorId::kGrey,
           {GURL("https://a.com")});
  ExpectGroupTitlesInMenu({"Group 1"});

  AddGroup(u"Group 2", tab_groups::TabGroupColorId::kBlue,
           {GURL("https://b.com")});
  ExpectGroupTitlesInMenu({"Group 1", "Group 2"});

  const auto& groups_before_remove = service()->GetAllGroups();
  ASSERT_EQ(2u, groups_before_remove.size());
  base::Uuid group1_uuid = groups_before_remove[0].saved_guid();
  if (groups_before_remove[0].title() != u"Group 1") {
    group1_uuid = groups_before_remove[1].saved_guid();
  }
  service()->RemoveGroup(group1_uuid);
  ExpectGroupTitlesInMenu({"Group 2"});
}

TEST_F(TabGroupMenuBridgeTest, SubmenuHasCorrectItems) {
  TabGroupMenuBridge bridge(profile(), service());
  AddGroup(u"Group 1", tab_groups::TabGroupColorId::kGrey,
           {GURL("https://a.com"), GURL("https://b.com")});
  bridge.BuildMenu();

  ExpectGroupTitlesInMenu({"Group 1"});
  NSMenuItem* group_item = [menu() itemWithTitle:@"Group 1"];
  ASSERT_TRUE(group_item);
  ASSERT_TRUE(group_item.hasSubmenu);
  NSMenu* submenu = group_item.submenu;

  // Expected items:
  // 0: Open in Browser
  // 1: Open/Move to New Window
  // 2: Pin/Unpin
  // 3: Delete/Leave
  // 4: --- separator ---
  // 5: Tab 1
  // 6: Tab 2
  EXPECT_EQ(7, submenu.numberOfItems);
  EXPECT_EQ(base::SysNSStringToUTF16([[submenu itemAtIndex:0] title]),
            l10n_util::GetStringUTF16(IDS_OPEN_GROUP_IN_BROWSER_MENU));
  EXPECT_EQ(base::SysNSStringToUTF16([[submenu itemAtIndex:1] title]),
            l10n_util::GetStringUTF16(
                IDS_TAB_GROUP_HEADER_CXMENU_OPEN_GROUP_IN_NEW_WINDOW));
  EXPECT_EQ(base::SysNSStringToUTF16([[submenu itemAtIndex:2] title]),
            l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_PIN_GROUP));
  EXPECT_EQ(
      base::SysNSStringToUTF16([[submenu itemAtIndex:3] title]),
      l10n_util::GetStringUTF16(IDS_TAB_GROUP_HEADER_CXMENU_DELETE_GROUP));
  EXPECT_TRUE([[submenu itemAtIndex:4] isSeparatorItem]);
  EXPECT_EQ(base::SysNSStringToUTF8([[submenu itemAtIndex:5] title]),
            "Tab Title");
  EXPECT_EQ(base::SysNSStringToUTF8([[submenu itemAtIndex:6] title]),
            "Tab Title");
}

TEST_F(TabGroupMenuBridgeTest, ClickingTabOpensUrl) {
  TabGroupMenuBridge bridge(profile(), service());

  AddGroup(u"Group 1", tab_groups::TabGroupColorId::kGrey,
           {GURL("https://a.com")});

  NSMenuItem* group_item = [menu() itemWithTitle:@"Group 1"];
  ASSERT_TRUE(group_item);
  NSMenu* submenu = group_item.submenu;
  NSMenuItem* tab_item = nil;
  for (NSMenuItem* item in [submenu itemArray]) {
    if ([item.title isEqualToString:@"Tab Title"]) {
      tab_item = item;
      break;
    }
  }
  ASSERT_TRUE(tab_item);

  EXPECT_EQ(0, browser()->tab_strip_model()->count());

  [submenu performActionForItemAtIndex:[submenu indexOfItem:tab_item]];

  EXPECT_EQ(1, browser()->tab_strip_model()->count());
  content::WebContents* new_tab =
      browser()->tab_strip_model()->GetWebContentsAt(0);
  EXPECT_EQ(GURL("https://a.com"), new_tab->GetVisibleURL());
}
