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

#import "chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar.h"

#import <Cocoa/Cocoa.h>

#include "base/apple/foundation_util.h"
#include "base/mac/mac_util.h"
#include "base/memory/raw_ptr.h"
#include "base/strings/sys_string_conversions.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/command_updater.h"
#include "chrome/browser/search_engines/template_url_service_factory_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_command_controller.h"
#include "chrome/browser/ui/browser_window/public/browser_collection_observer.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection_platform_delegate.h"
#include "chrome/browser/ui/cocoa/test/cocoa_test_helper.h"
#include "chrome/browser/ui/exclusive_access/exclusive_access_manager.h"
#include "chrome/browser/ui/exclusive_access/fullscreen_controller.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/browser_with_test_window_test.h"
#include "components/prefs/pref_service.h"
#include "components/strings/grit/components_strings.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"
#import "ui/base/cocoa/touch_bar_util.h"
#include "ui/base/l10n/l10n_util_mac.h"

class BrowserWindowDefaultTouchBarUnitTest : public BrowserWithTestWindowTest {
 public:
  void SetUp() override {
    BrowserWithTestWindowTest::SetUp();

    // The touch bar can show a search engine prompt, which requires this
    // service.
    template_service_util_ =
        std::make_unique<TemplateURLServiceFactoryTestUtil>(profile());
    template_service_util_->VerifyLoad();

    command_updater_ = chrome::BrowserCommandController::From(browser());

    browser()->tab_strip_model()->AppendWebContents(
        content::WebContentsTester::CreateTestWebContents(profile(), nullptr),
        true);

    touch_bar_ = [[BrowserWindowDefaultTouchBar alloc] init];
    touch_bar_.browser = browser();
  }

  void UpdateCommandEnabled(int id, bool enabled) {
    command_updater_->UpdateCommandEnabled(id, enabled);
  }

  bool ShowsHomeButton() {
    return browser()->GetProfile()->GetPrefs()->GetBoolean(
        prefs::kShowHomeButton);
  }

  void SetShowHomeButton(bool flag) {
    browser()->GetProfile()->GetPrefs()->SetBoolean(prefs::kShowHomeButton,
                                                    flag);
  }

  void TearDown() override {
    touch_bar_.browser = nullptr;
    touch_bar_ = nil;

    BrowserWithTestWindowTest::TearDown();
  }

  CocoaTestHelper cocoa_test_helper_;
  raw_ptr<CommandUpdater, DanglingUntriaged>
      command_updater_;  // Weak, owned by Browser.

  std::unique_ptr<TemplateURLServiceFactoryTestUtil> template_service_util_;

  BrowserWindowDefaultTouchBar* __strong touch_bar_;
};

// Test if any known identifiers no longer work. See the message in the test;
// these identifiers may be written out to disk on users' computers if they
// customize the Touch Bar, and the corresponding items will disappear if they
// can no longer be created.
TEST_F(BrowserWindowDefaultTouchBarUnitTest, HistoricTouchBarItems) {
  NSTouchBar* touch_bar = [touch_bar_ makeTouchBar];
  for (NSString* item_identifier : {
           @"BACK-FWD",
           @"BACK",
           @"FORWARD",
           @"RELOAD-STOP",
           @"HOME",
           @"SEARCH",
           @"BOOKMARK",
           @"NEW-TAB",
       }) {
    auto identifier = ui::GetTouchBarItemId(@"browser-window", item_identifier);
    EXPECT_NE(nil, [touch_bar itemForIdentifier:identifier])
        << "BrowserWindowDefaultTouchBar didn't return a Touch Bar item for "
           "an identifier that was once available ("
        << identifier.UTF8String
        << "). If a user's customized Touch Bar includes this item, it will "
           "disappear! Do not update or remove entries in this list just to "
           "make the test pass; keep supporting old identifiers when "
           "possible, even if they're no longer part of the set of "
           "default/customizable items.";
  }
}

// Tests if BrowserWindowDefaultTouchBar can produce the items it says it can
// and, for each kind of bar, also verify that the advertised/customizable lists
// include some representative items (if not, the lists might be wrong.)
TEST_F(BrowserWindowDefaultTouchBarUnitTest, TouchBarItems) {
  auto test_default_identifiers = [&](NSSet* expected_identifiers) {
    NSTouchBar* touch_bar = [touch_bar_ makeTouchBar];
    NSMutableSet<NSString*>* advertised_identifiers = [NSMutableSet set];
    [advertised_identifiers
        addObjectsFromArray:touch_bar.defaultItemIdentifiers];
    [advertised_identifiers
        addObjectsFromArray:touch_bar.customizationAllowedItemIdentifiers];
    [advertised_identifiers
        addObjectsFromArray:touch_bar.customizationRequiredItemIdentifiers];
    EXPECT_TRUE([expected_identifiers isSubsetOfSet:advertised_identifiers])
        << "Didn't find the expected identifiers "
        << expected_identifiers.description.UTF8String
        << " in the set of advertised identifiers "
        << advertised_identifiers.description.UTF8String << ".";
    for (NSString* identifier in advertised_identifiers) {
      EXPECT_NE(nil, [touch_bar itemForIdentifier:identifier])
          << "Didn't get a touch bar item for " << identifier.UTF8String;
    }
  };

  // Set to tab fullscreen.
  FullscreenController* fullscreen_controller = browser()
                                                    ->GetFeatures()
                                                    .exclusive_access_manager()
                                                    ->fullscreen_controller();
  fullscreen_controller->set_is_tab_fullscreen_for_testing(true);
  EXPECT_TRUE(fullscreen_controller->IsTabFullscreen());

  // The fullscreen Touch Bar should include *at least* these items.
  test_default_identifiers([NSSet setWithArray:@[
    BrowserWindowDefaultTouchBar.fullscreenOriginItemIdentifier,
  ]]);

  // Exit fullscreen.
  fullscreen_controller->set_is_tab_fullscreen_for_testing(false);
  EXPECT_FALSE(fullscreen_controller->IsTabFullscreen());

  // The default Touch Bar should include *at least* these items.
  test_default_identifiers([NSSet setWithArray:@[
    BrowserWindowDefaultTouchBar.backItemIdentifier,
    BrowserWindowDefaultTouchBar.forwardItemIdentifier,
    BrowserWindowDefaultTouchBar.reloadOrStopItemIdentifier,
  ]]);
}

// Tests the reload or stop touch bar item.
TEST_F(BrowserWindowDefaultTouchBarUnitTest, ReloadOrStopTouchBarItem) {
  NSTouchBar* touch_bar = [touch_bar_ makeTouchBar];
  [touch_bar_ setIsPageLoading:NO];

  NSTouchBarItem* item =
      [touch_bar itemForIdentifier:BrowserWindowDefaultTouchBar
                                       .reloadOrStopItemIdentifier];
  NSButton* button = base::apple::ObjCCast<NSButton>([item view]);
  EXPECT_EQ(IDC_RELOAD, [button tag]);
  EXPECT_EQ([BrowserWindowDefaultTouchBar reloadIcon], [button image]);

  [touch_bar_ setIsPageLoading:YES];
  item = [touch_bar itemForIdentifier:BrowserWindowDefaultTouchBar
                                          .reloadOrStopItemIdentifier];
  button = base::apple::ObjCCast<NSButton>([item view]);
  EXPECT_EQ(IDC_STOP, [button tag]);
  EXPECT_EQ([BrowserWindowDefaultTouchBar navigateStopIcon], [button image]);
}

// Tests the bookmark star touch bar item.
TEST_F(BrowserWindowDefaultTouchBarUnitTest, BookmkarStarTouchBarItem) {
  NSTouchBar* touch_bar = [touch_bar_ makeTouchBar];

  [touch_bar_ setIsStarred:NO];
  NSTouchBarItem* item =
      [touch_bar itemForIdentifier:BrowserWindowDefaultTouchBar
                                       .bookmarkStarItemIdentifier];
  NSButton* button = base::apple::ObjCCast<NSButton>([item view]);
  EXPECT_EQ([BrowserWindowDefaultTouchBar starDefaultIcon], [button image]);

  [touch_bar_ setIsStarred:YES];
  item = [touch_bar itemForIdentifier:BrowserWindowDefaultTouchBar
                                          .bookmarkStarItemIdentifier];
  button = base::apple::ObjCCast<NSButton>([item view]);
  EXPECT_EQ([BrowserWindowDefaultTouchBar starActiveIcon], [button image]);
}

// Tests if the back button on the touch bar is in sync with the back command.
TEST_F(BrowserWindowDefaultTouchBarUnitTest, BackCommandUpdate) {
  NSTouchBar* touch_bar = [touch_bar_ makeTouchBar];
  NSTouchBarItem* item = [touch_bar
      itemForIdentifier:BrowserWindowDefaultTouchBar.backItemIdentifier];
  NSButton* button = base::apple::ObjCCast<NSButton>(item.view);

  UpdateCommandEnabled(IDC_BACK, true);
  EXPECT_TRUE(button.enabled);
  UpdateCommandEnabled(IDC_BACK, false);
  EXPECT_FALSE(button.enabled);
}

// Tests if the forward button on the touch bar is in sync with the forward
// command.
TEST_F(BrowserWindowDefaultTouchBarUnitTest, ForwardCommandUpdate) {
  NSTouchBar* touch_bar = [touch_bar_ makeTouchBar];
  NSTouchBarItem* item = [touch_bar
      itemForIdentifier:BrowserWindowDefaultTouchBar.forwardItemIdentifier];
  NSButton* button = base::apple::ObjCCast<NSButton>(item.view);

  UpdateCommandEnabled(IDC_FORWARD, true);
  EXPECT_TRUE(button.enabled);
  UpdateCommandEnabled(IDC_FORWARD, false);
  EXPECT_FALSE(button.enabled);
}

TEST_F(BrowserWindowDefaultTouchBarUnitTest, BackAccessibilityLabel) {
  NSTouchBar* touch_bar = [touch_bar_ makeTouchBar];
  NSTouchBarItem* item = [touch_bar
      itemForIdentifier:BrowserWindowDefaultTouchBar.backItemIdentifier];
  id<NSAccessibility> view = item.view;
  ASSERT_TRUE([view conformsToProtocol:@protocol(NSAccessibility)]);
  EXPECT_NSEQ(view.accessibilityTitle,
              l10n_util::GetNSString(IDS_ACCNAME_BACK));
}

TEST_F(BrowserWindowDefaultTouchBarUnitTest, ForwardAccessibilityLabel) {
  NSTouchBar* touch_bar = [touch_bar_ makeTouchBar];
  NSTouchBarItem* item = [touch_bar
      itemForIdentifier:BrowserWindowDefaultTouchBar.forwardItemIdentifier];
  id<NSAccessibility> view = item.view;
  ASSERT_TRUE([view conformsToProtocol:@protocol(NSAccessibility)]);
  EXPECT_NSEQ(view.accessibilityTitle,
              l10n_util::GetNSString(IDS_ACCNAME_FORWARD));
}

// Tests that the home button in the Touch Bar is in sync with the setting.
TEST_F(BrowserWindowDefaultTouchBarUnitTest, HomeUpdate) {
  NSTouchBar* touch_bar = [touch_bar_ makeTouchBar];

  // Save the current state before we start mucking with preferences.
  bool home_button_showing = ShowsHomeButton();

  SetShowHomeButton(false);
  touch_bar = [touch_bar_ makeTouchBar];

  NSString* home_identifier = [BrowserWindowDefaultTouchBar homeItemIdentifier];

  EXPECT_FALSE(
      [[touch_bar defaultItemIdentifiers] containsObject:home_identifier]);

  SetShowHomeButton(true);
  touch_bar = [touch_bar_ makeTouchBar];

  EXPECT_TRUE(
      [[touch_bar defaultItemIdentifiers] containsObject:home_identifier]);

  // Restore the original state.
  SetShowHomeButton(home_button_showing);
}

// Tests that closing a browser doesn't cause a use-after-free when resetting
// the browser property of the Touch Bar.
TEST_F(BrowserWindowDefaultTouchBarUnitTest, OnBrowserClosedNoCrash) {
  EXPECT_NE(nil, touch_bar_);
  EXPECT_EQ(browser(), touch_bar_.browser);

  // Simulate OnBrowserClosed from GlobalBrowserCollection.
  BrowserCollectionObserver* platform_delegate =
      GlobalBrowserCollection::GetInstance()->GetPlatformDelegate();
  platform_delegate->OnBrowserClosed(browser());

  // The Touch Bar's browser property should be reset, and the bridge destroyed.
  EXPECT_EQ(nullptr, touch_bar_.browser);
}
