// 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_touch_bar_controller.h"

#import "base/apple/foundation_util.h"
#include "base/apple/scoped_objc_class_swizzler.h"
#include "chrome/app/chrome_command_ids.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/ui/bookmarks/bookmark_tab_helper.h"
#include "chrome/browser/ui/bookmarks/bookmark_tab_helper_observer.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_commands.h"
#include "chrome/browser/ui/browser_window.h"
#import "chrome/browser/ui/cocoa/touchbar/browser_window_default_touch_bar.h"
#include "chrome/browser/ui/views/frame/browser_native_widget_mac.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/prefs/pref_service.h"
#include "components/remote_cocoa/app_shim/window_touch_bar_delegate.h"
#include "components/search_engines/default_search_manager.h"
#include "components/search_engines/search_engines_test_util.h"
#include "components/search_engines/template_url_data.h"
#include "components/search_engines/template_url_data_util.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "content/public/test/test_utils.h"
#include "testing/gtest_mac.h"
#include "ui/gfx/native_ui_types.h"

// A class that watches for invalidations of a window's touch bar by calls
// to -setTouchBar:.
//
// Why is this class structured the way it is, with statics for the invalidation
// flag and the swizzler? We want to catch calls to -setTouchBar:. We can use
// swizzling for that. We implement -setTouchBar: in this class and swap it for
// the NSWindow implementation. Once inside TouchBarInvalidationWatcher's
// -setTouchBar: we need to set a flag somewhere to the value of (aTouchBar ==
// nil). In theory that flag could just be an instance variable of this class.
// The problem is when we reach -setTouchBar:, although the code comes from
// TouchBarInvalidationWatcher, the instance "executing" the code is an
// NSWindow. If we create an instance of TouchBarInvalidationWatcher that has a
// bool instance variable, say, the NSWindow can't access it (it's not the
// original TouchBarInvalidationWatcher instance). Similarly, at the end of
// -setTouchBar: we need to call the original (NSWindow) implementation of
// -setTouchBar:. To do that we need access to the ScopedObjCClassSwizzler that
// did the swap. If we store the ScopedObjCClassSwizzler in an instance variable
// of TouchBarInvalidationWatcher, our NSWindow instance again cannot access it.
//
// To get around these problems we store the flag and the swizzler in what are
// essentially class variables so they are accessible from anywhere.
@interface TouchBarInvalidationWatcher : NSObject
// Returns a new (non-autoreleased) TouchBarInvalidationWatcher.
+ (instancetype)newWatcher;

// Returns the touch bar invalidation flag. This flag is set to YES
// whenever -[NSWindow setTouchBar:] is called with nil.
+ (BOOL&)touchBarInvalidFlag;
@end

@implementation TouchBarInvalidationWatcher

+ (BOOL&)touchBarInvalidFlag {
  static BOOL touchBarInvalidFlag = NO;
  return touchBarInvalidFlag;
}

+ (std::unique_ptr<base::apple::ScopedObjCClassSwizzler>&)setTouchBarSwizzler {
  static base::NoDestructor<
      std::unique_ptr<base::apple::ScopedObjCClassSwizzler>>
      setTouchBarSwizzler(new base::apple::ScopedObjCClassSwizzler(
          [NSWindow class], [TouchBarInvalidationWatcher class],
          @selector(setTouchBar:)));

  return *setTouchBarSwizzler;
}

+ (instancetype)newWatcher {
  // Set up the swizzling.
  [self setTouchBarSwizzler];

  return [[TouchBarInvalidationWatcher alloc] init];
}

- (void)dealloc {
  [TouchBarInvalidationWatcher setTouchBarSwizzler].reset();
}

- (void)setTouchBar:(NSTouchBar*)aTouchBar {
  [TouchBarInvalidationWatcher touchBarInvalidFlag] = (aTouchBar == nil);

  // Proceed with setting the touch bar.
  [TouchBarInvalidationWatcher setTouchBarSwizzler]
      ->InvokeOriginal<void, NSTouchBar*>(self, @selector(setTouchBar:),
                                          aTouchBar);
}

@end

// A class that watches for page reload notifications in a window's touch bar.
//
// See the explanation at the top of TouchBarInvalidationWatcher for info on
// why this class is structured the way it is.
@interface PageReloadWatcher : NSObject
// Returns a new (non-autoreleased) PageReloadWatcher.
+ (instancetype)newWatcher;

// Returns the page loading flag. This flag is set to YES whenever
// -[BrowserWindowDefaultTouchBar setIsPageLoading:] is called with YES.
+ (BOOL&)pageIsLoadingFlag;
@end

@implementation PageReloadWatcher

+ (BOOL&)pageIsLoadingFlag {
  static BOOL pageIsLoadingFlag = NO;
  return pageIsLoadingFlag;
}

+ (std::unique_ptr<base::apple::ScopedObjCClassSwizzler>&)
    setPageIsLoadingSwizzler {
  static base::NoDestructor<
      std::unique_ptr<base::apple::ScopedObjCClassSwizzler>>
      setPageIsLoadingSwizzler(new base::apple::ScopedObjCClassSwizzler(
          [BrowserWindowDefaultTouchBar class], [PageReloadWatcher class],
          @selector(setIsPageLoading:)));

  return *setPageIsLoadingSwizzler;
}

+ (instancetype)newWatcher {
  // Set up the swizzling.
  [self setPageIsLoadingSwizzler];

  return [[PageReloadWatcher alloc] init];
}

- (void)dealloc {
  [PageReloadWatcher setPageIsLoadingSwizzler].reset();
}

- (void)setIsPageLoading:(BOOL)flag {
  if (flag) {
    [PageReloadWatcher pageIsLoadingFlag] = YES;
  }

  [PageReloadWatcher setPageIsLoadingSwizzler]->InvokeOriginal<void, BOOL>(
      self, @selector(setIsPageLoading:), flag);
}

@end

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

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

  NSTouchBar* MakeTouchBar() {
    auto* delegate =
        static_cast<NSObject<WindowTouchBarDelegate>*>(ns_window());
    return [delegate makeTouchBar];
  }

  gfx::NativeWindow native_window() const {
    return browser()->GetWindow()->GetNativeWindow();
  }

  NSWindow* ns_window() const { return native_window().GetNativeNSWindow(); }

  BrowserWindowTouchBarController* browser_touch_bar_controller() const {
    BrowserView* browser_view =
        BrowserView::GetBrowserViewForNativeWindow(native_window());
    EXPECT_TRUE(browser_view);
    if (!browser_view) {
      return nil;
    }

    BrowserNativeWidgetMac* native_widget =
        static_cast<BrowserNativeWidgetMac*>(
            browser_view->browser_widget()->browser_native_widget());
    return native_widget->GetTouchBarController();
  }
};

// Test if the touch bar gets invalidated when the active tab is changed.
IN_PROC_BROWSER_TEST_F(BrowserWindowTouchBarControllerTest, TabChanges) {
  [[maybe_unused]] TouchBarInvalidationWatcher* invalidationWatcher =
      [TouchBarInvalidationWatcher newWatcher];

  EXPECT_FALSE(browser_touch_bar_controller());
  MakeTouchBar();
  EXPECT_TRUE(browser_touch_bar_controller());

  auto* current_touch_bar = [ns_window() touchBar];
  EXPECT_TRUE(current_touch_bar);

  // Insert a new tab in the foreground. The window should invalidate its
  // touch bar as a result.
  [TouchBarInvalidationWatcher touchBarInvalidFlag] = NO;
  ASSERT_FALSE([TouchBarInvalidationWatcher touchBarInvalidFlag]);
  std::unique_ptr<content::WebContents> contents = content::WebContents::Create(
      content::WebContents::CreateParams(browser()->GetProfile()));
  browser()->tab_strip_model()->AppendWebContents(std::move(contents), true);

  EXPECT_TRUE([TouchBarInvalidationWatcher touchBarInvalidFlag]);

  // Update the touch bar.
  [ns_window() touchBar];

  // Activating the original tab should invalidate the touch bar.
  [TouchBarInvalidationWatcher touchBarInvalidFlag] = NO;
  ASSERT_FALSE([TouchBarInvalidationWatcher touchBarInvalidFlag]);
  browser()->tab_strip_model()->ActivateTabAt(0);

  EXPECT_TRUE([TouchBarInvalidationWatcher touchBarInvalidFlag]);
}

// Test if the touch bar receives a notification that the current tab is
// loading.
IN_PROC_BROWSER_TEST_F(BrowserWindowTouchBarControllerTest, PageReload) {
  [[maybe_unused]] PageReloadWatcher* pageReloadWatcher =
      [PageReloadWatcher newWatcher];

  EXPECT_FALSE(browser_touch_bar_controller());
  MakeTouchBar();
  EXPECT_TRUE(browser_touch_bar_controller());

  // Make sure the touch bar exists for the window.
  auto* current_touch_bar = [ns_window() touchBar];
  EXPECT_TRUE(current_touch_bar);

  // We can't just ask the BrowserWindowDefaultTouchBar for the value of the
  // page loading flag like we can for the tab bookmark. The reload my happen
  // so fast that the flag may be reset to NO by the time we check it. We
  // have to use swizzling instead.
  [PageReloadWatcher pageIsLoadingFlag] = NO;
  ASSERT_FALSE([PageReloadWatcher pageIsLoadingFlag]);

  ASSERT_TRUE(ui_test_utils::NavigateToURL(
      browser(), GURL("data:text/html, <html><body></body></html>")));

  EXPECT_TRUE([PageReloadWatcher pageIsLoadingFlag]);
}

// Test if the touch bar receives a notification that the current tab has been
// bookmarked.
IN_PROC_BROWSER_TEST_F(BrowserWindowTouchBarControllerTest,
                       BookmarkCurrentTab) {
  EXPECT_FALSE(browser_touch_bar_controller());
  MakeTouchBar();
  EXPECT_TRUE(browser_touch_bar_controller());

  // Make sure the touch bar exists for the window.
  auto* current_touch_bar = [ns_window() touchBar];
  EXPECT_TRUE(current_touch_bar);
  BrowserWindowDefaultTouchBar* touch_bar_delegate =
      base::apple::ObjCCastStrict<BrowserWindowDefaultTouchBar>(
          [current_touch_bar delegate]);
  EXPECT_FALSE([touch_bar_delegate isStarred]);

  chrome::BookmarkCurrentTab(browser());

  EXPECT_TRUE([touch_bar_delegate isStarred]);
}

// Tests if the touch bar's search button updates if the default search engine
// has changed.
IN_PROC_BROWSER_TEST_F(BrowserWindowTouchBarControllerTest,
                       SearchEngineChanges) {
  [[maybe_unused]] TouchBarInvalidationWatcher* invalidationWatcher =
      [TouchBarInvalidationWatcher newWatcher];

  PrefService* prefs = browser()->GetProfile()->GetPrefs();
  DCHECK(prefs);

  EXPECT_FALSE(browser_touch_bar_controller());
  MakeTouchBar();

  // Force the window to create the touch bar.
  [ns_window() touchBar];
  NSString* orig_search_button_title =
      [[[browser_touch_bar_controller() defaultTouchBar] searchButton] title];
  EXPECT_TRUE(orig_search_button_title);

  // Change the default search engine.
  [TouchBarInvalidationWatcher touchBarInvalidFlag] = NO;
  ASSERT_FALSE([TouchBarInvalidationWatcher touchBarInvalidFlag]);
  std::unique_ptr<TemplateURLData> data =
      GenerateDummyTemplateURLData("poutine");
  prefs->SetDict(DefaultSearchManager::kDefaultSearchProviderDataPrefName,
                 TemplateURLDataToDictionary(*data));

  // Confirm the touch bar was invalidated.
  EXPECT_TRUE([TouchBarInvalidationWatcher touchBarInvalidFlag]);

  // Ask the window again for its touch bar. Previously, changes like updates
  // to the default search engine would completely regenerate the touch bar.
  // That's expensive (view creation, autolayout, etc.). Instead we now retain
  // the original touch bar and expect touch bar invalidation to force an
  // update of the search item.
  [ns_window() touchBar];
  EXPECT_FALSE([orig_search_button_title
      isEqualToString:[[[browser_touch_bar_controller() defaultTouchBar]
                          searchButton] title]]);
}

// Tests to see if the touch bar's bookmark tab helper observer gets removed
// when the touch bar is destroyed.
IN_PROC_BROWSER_TEST_F(BrowserWindowTouchBarControllerTest,
                       DestroyNotificationBridge) {
  MakeTouchBar();

  ASSERT_TRUE([browser_touch_bar_controller() defaultTouchBar]);

  BookmarkTabHelperObserver* observer =
      [[browser_touch_bar_controller() defaultTouchBar] bookmarkTabObserver];
  std::unique_ptr<content::WebContents> contents = content::WebContents::Create(
      content::WebContents::CreateParams(browser()->GetProfile()));
  browser()->tab_strip_model()->AppendWebContents(std::move(contents), true);

  BookmarkTabHelper* tab_helper = BookmarkTabHelper::FromWebContents(
      browser()->tab_strip_model()->GetActiveWebContents());
  ASSERT_TRUE(tab_helper);
  EXPECT_TRUE(tab_helper->HasObserver(observer));

  [[browser_touch_bar_controller() defaultTouchBar] setBrowser:nullptr];
  EXPECT_FALSE(tab_helper->HasObserver(observer));
}
