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

#import <Cocoa/Cocoa.h>

#include "base/apple/scoped_objc_class_swizzler.h"
#include "base/files/file_path.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/run_loop.h"
#include "base/strings/utf_string_conversions.h"
#include "base/test/bind.h"
#include "chrome/app/chrome_command_ids.h"
#import "chrome/browser/app_controller_mac.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/profiles/delete_profile_helper.h"
#include "chrome/browser/profiles/profile_manager.h"
#include "chrome/browser/profiles/profile_metrics.h"
#include "chrome/common/chrome_constants.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/testing_browser_process.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/base/testing_profile_manager.h"
#include "content/public/test/browser_task_environment.h"
#include "testing/platform_test.h"

namespace {

id __weak* TargetForAction() {
  static id __weak targetForAction;
  return &targetForAction;
}

}  // namespace

@interface FakeBrowserWindow : NSWindow
@end

@implementation FakeBrowserWindow
@end

// A class providing alternative implementations of various methods.
@interface AppControllerKeyEquivalentTestHelper : NSObject
- (id __weak)targetForAction:(SEL)selector;
- (BOOL)windowHasBrowserTabs:(NSWindow*)window;
@end

@implementation AppControllerKeyEquivalentTestHelper

- (id __weak)targetForAction:(SEL)selector {
  return *TargetForAction();
}

- (BOOL)windowHasBrowserTabs:(NSWindow*)window {
  return [window isKindOfClass:[FakeBrowserWindow class]];
}

@end

class AppControllerTest : public PlatformTest {
 protected:
  AppControllerTest()
      : profile_manager_(TestingBrowserProcess::GetGlobal()),
        profile_(nullptr) {}

  void SetUp() override {
    PlatformTest::SetUp();
    ASSERT_TRUE(profile_manager_.SetUp());
    profile_ = profile_manager_.CreateTestingProfile("New Profile 1");
  }

  void TearDown() override {
    TestingBrowserProcess::GetGlobal()->SetProfileManager(nullptr);
    base::RunLoop().RunUntilIdle();
    PlatformTest::TearDown();
  }

  content::BrowserTaskEnvironment task_environment_;
  TestingProfileManager profile_manager_;
  raw_ptr<TestingProfile, DanglingUntriaged> profile_;
};

class AppControllerKeyEquivalentTest : public PlatformTest {
 protected:
  AppControllerKeyEquivalentTest() = default;

  void SetUp() override {
    PlatformTest::SetUp();

    nsapp_target_for_action_swizzler_ =
        std::make_unique<base::apple::ScopedObjCClassSwizzler>(
            [NSApp class], [AppControllerKeyEquivalentTestHelper class],
            @selector(targetForAction:));
    app_controller_swizzler_ =
        std::make_unique<base::apple::ScopedObjCClassSwizzler>(
            [AppController class], [AppControllerKeyEquivalentTestHelper class],
            @selector(windowHasBrowserTabs:));

    app_controller_ = AppController.sharedController;

    close_window_menu_item_ = [[NSMenuItem alloc] initWithTitle:@""
                                                         action:nullptr
                                                  keyEquivalent:@""];
    [app_controller_ setCloseWindowMenuItemForTesting:close_window_menu_item_];

    close_tab_menu_item_ = [[NSMenuItem alloc] initWithTitle:@""
                                                      action:nullptr
                                               keyEquivalent:@""];
    [app_controller_ setCloseTabMenuItemForTesting:close_tab_menu_item_];
  }

  void CheckMenuItemsMatchBrowserWindow() {
    ASSERT_EQ([NSApp targetForAction:@selector(performClose:)],
              *TargetForAction());

    [app_controller_ updateMenuItemKeyEquivalents];

    EXPECT_TRUE([[close_window_menu_item_ keyEquivalent] isEqualToString:@"W"]);
    EXPECT_EQ([close_window_menu_item_ keyEquivalentModifierMask],
              NSEventModifierFlagCommand);
    EXPECT_TRUE([[close_tab_menu_item_ keyEquivalent] isEqualToString:@"w"]);
    EXPECT_EQ([close_tab_menu_item_ keyEquivalentModifierMask],
              NSEventModifierFlagCommand);
  }

  void CheckMenuItemsMatchNonBrowserWindow() {
    ASSERT_EQ([NSApp targetForAction:@selector(performClose:)],
              *TargetForAction());

    [app_controller_ updateMenuItemKeyEquivalents];

    EXPECT_TRUE([[close_window_menu_item_ keyEquivalent] isEqualToString:@"w"]);
    EXPECT_EQ([close_window_menu_item_ keyEquivalentModifierMask],
              NSEventModifierFlagCommand);
    EXPECT_TRUE([[close_tab_menu_item_ keyEquivalent] isEqualToString:@""]);
    EXPECT_EQ([close_tab_menu_item_ keyEquivalentModifierMask], 0UL);
  }

  void TearDown() override {
    PlatformTest::TearDown();

    [app_controller_ setCloseWindowMenuItemForTesting:nil];
    [app_controller_ setCloseTabMenuItemForTesting:nil];
    *TargetForAction() = nil;
  }

 private:
  std::unique_ptr<base::apple::ScopedObjCClassSwizzler>
      nsapp_target_for_action_swizzler_;
  std::unique_ptr<base::apple::ScopedObjCClassSwizzler>
      app_controller_swizzler_;
  AppController* __strong app_controller_;
  NSMenuItem* __strong close_window_menu_item_;
  NSMenuItem* __strong close_tab_menu_item_;
};

TEST_F(AppControllerTest, DockMenuProfileNotLoaded) {
  AppController* app_controller = AppController.sharedController;
  NSMenu* menu = [app_controller applicationDockMenu:NSApp];
  // Incognito item is hidden when the profile is not loaded.
  EXPECT_EQ(nil, [app_controller lastProfileIfLoaded]);
  EXPECT_EQ(-1, [menu indexOfItemWithTag:IDC_NEW_INCOGNITO_WINDOW]);
}

TEST_F(AppControllerTest, DockMenu) {
  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed,
                         profile_->GetPath().BaseName().MaybeAsASCII());

  AppController* app_controller = AppController.sharedController;
  NSMenu* menu = [app_controller applicationDockMenu:NSApp];
  NSMenuItem* item;

  EXPECT_TRUE(menu);
  EXPECT_NE(-1, [menu indexOfItemWithTag:IDC_NEW_WINDOW]);

  // Incognito item is shown when the profile is loaded.
  EXPECT_EQ(profile_, [app_controller lastProfileIfLoaded]);
  EXPECT_NE(-1, [menu indexOfItemWithTag:IDC_NEW_INCOGNITO_WINDOW]);

  for (item in [menu itemArray]) {
    EXPECT_EQ(app_controller, [item target]);
    EXPECT_EQ(@selector(commandFromDock:), [item action]);
  }
}

TEST_F(AppControllerTest, LastProfileIfLoaded) {
  // Create a second profile.
  base::FilePath dest_path1 = profile_->GetPath();
  base::FilePath dest_path2 =
      profile_manager_.CreateTestingProfile("New Profile 2")->GetPath();
  ASSERT_EQ(2U, profile_manager_.profile_manager()->GetNumberOfProfiles());
  ASSERT_EQ(2U, profile_manager_.profile_manager()->GetLoadedProfiles().size());

  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed,
                         dest_path1.BaseName().MaybeAsASCII());

  AppController* app_controller = AppController.sharedController;

  // Delete the active profile.
  profile_manager_.profile_manager()
      ->GetDeleteProfileHelper()
      .MaybeScheduleProfileForDeletion(
          dest_path1, base::DoNothing(),
          ProfileMetrics::DELETE_PROFILE_USER_MANAGER);

  base::RunLoop().RunUntilIdle();

  EXPECT_EQ(dest_path2, app_controller.lastProfileIfLoaded->GetPath());
}

// Tests key equivalents for Close Window when target is a child window (like a
// bubble).
TEST_F(AppControllerKeyEquivalentTest, UpdateMenuItemsForBubbleWindow) {
  // Set up the "bubble" and main window.
  const NSRect kContentRect = NSMakeRect(0.0, 0.0, 10.0, 10.0);
  NSWindow* child_window =
      [[NSWindow alloc] initWithContentRect:kContentRect
                                  styleMask:NSWindowStyleMaskClosable
                                    backing:NSBackingStoreBuffered
                                      defer:YES];
  child_window.releasedWhenClosed = NO;
  NSWindow* browser_window =
      [[FakeBrowserWindow alloc] initWithContentRect:kContentRect
                                           styleMask:NSWindowStyleMaskClosable
                                             backing:NSBackingStoreBuffered
                                               defer:YES];
  browser_window.releasedWhenClosed = NO;

  [browser_window addChildWindow:child_window ordered:NSWindowAbove];

  *TargetForAction() = child_window;

  CheckMenuItemsMatchBrowserWindow();
}

// Tests key equivalents for Close Window when target is an NSPopOver.
TEST_F(AppControllerKeyEquivalentTest, UpdateMenuItemsForPopover) {
  // Set up the popover and main window.
  const NSRect kContentRect = NSMakeRect(0.0, 0.0, 10.0, 10.0);
  NSPopover* popover = [[NSPopover alloc] init];
  NSWindow* popover_window =
      [[NSWindow alloc] initWithContentRect:kContentRect
                                  styleMask:NSWindowStyleMaskClosable
                                    backing:NSBackingStoreBuffered
                                      defer:YES];
  popover_window.releasedWhenClosed = NO;

  [popover setContentViewController:[[NSViewController alloc] init]];
  [[popover contentViewController] setView:[popover_window contentView]];

  NSWindow* browser_window =
      [[FakeBrowserWindow alloc] initWithContentRect:kContentRect
                                           styleMask:NSWindowStyleMaskClosable
                                             backing:NSBackingStoreBuffered
                                               defer:YES];
  browser_window.releasedWhenClosed = NO;
  [browser_window addChildWindow:popover_window ordered:NSWindowAbove];

  *TargetForAction() = popover;

  CheckMenuItemsMatchBrowserWindow();
}

// Tests key equivalents for Close Window when target is a browser window.
TEST_F(AppControllerKeyEquivalentTest, UpdateMenuItemsForBrowserWindow) {
  // Set up the browser window.
  const NSRect kContentRect = NSMakeRect(0.0, 0.0, 10.0, 10.0);
  NSWindow* browser_window =
      [[FakeBrowserWindow alloc] initWithContentRect:kContentRect
                                           styleMask:NSWindowStyleMaskClosable
                                             backing:NSBackingStoreBuffered
                                               defer:YES];

  *TargetForAction() = browser_window;

  CheckMenuItemsMatchBrowserWindow();
}

// Tests key equivalents for Close Window when target is not a browser window.
TEST_F(AppControllerKeyEquivalentTest, UpdateMenuItemsForNonBrowserWindow) {
  // Set up the window.
  const NSRect kContentRect = NSMakeRect(0.0, 0.0, 10.0, 10.0);
  NSWindow* main_window =
      [[NSWindow alloc] initWithContentRect:kContentRect
                                  styleMask:NSWindowStyleMaskClosable
                                    backing:NSBackingStoreBuffered
                                      defer:YES];

  *TargetForAction() = main_window;

  CheckMenuItemsMatchNonBrowserWindow();
}

// Tests key equivalents for Close Window when target is not a window.
TEST_F(AppControllerKeyEquivalentTest, UpdateMenuItemsForNonWindow) {
  NSObject* non_window_object = [[NSObject alloc] init];
  *TargetForAction() = non_window_object;

  CheckMenuItemsMatchNonBrowserWindow();
}

class AppControllerSafeProfileTest : public AppControllerTest {
 protected:
  AppControllerSafeProfileTest() = default;
  ~AppControllerSafeProfileTest() override = default;
};

// Tests that RunInLastProfileSafely() works with an already-loaded
// profile.
TEST_F(AppControllerSafeProfileTest, LastProfileLoaded) {
  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed,
                         profile_->GetPath().BaseName().MaybeAsASCII());

  AppController* app_controller = AppController.sharedController;
  ASSERT_EQ(profile_, app_controller.lastProfileIfLoaded);

  base::RunLoop run_loop;
  app_controller_mac::RunInLastProfileSafely(
      base::BindLambdaForTesting([&](Profile* profile) {
        EXPECT_EQ(profile, profile_.get());
        run_loop.Quit();
      }),
      app_controller_mac::kIgnoreOnFailure);
  run_loop.Run();
}

// Tests that RunInLastProfileSafely() re-loads the profile from disk if
// it's not currently in memory.
TEST_F(AppControllerSafeProfileTest, LastProfileNotLoaded) {
  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed, "New Profile 2");

  AppController* app_controller = AppController.sharedController;
  ASSERT_EQ(nil, app_controller.lastProfileIfLoaded);

  base::RunLoop run_loop;
  app_controller_mac::RunInLastProfileSafely(
      base::BindLambdaForTesting([&](Profile* profile) {
        EXPECT_NE(profile, nullptr);
        EXPECT_NE(profile, profile_.get());
        EXPECT_EQ(profile->GetBaseName().MaybeAsASCII(), "New Profile 2");
        run_loop.Quit();
      }),
      app_controller_mac::kIgnoreOnFailure);
  run_loop.Run();
}

// Tests that RunInProfileInSafeProfileHelper::RunInProfile() works with an
// already-loaded profile.
TEST_F(AppControllerSafeProfileTest, SpecificProfileLoaded) {
  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed,
                         profile_->GetPath().BaseName().MaybeAsASCII());

  AppController* app_controller = AppController.sharedController;
  ASSERT_EQ(profile_, app_controller.lastProfileIfLoaded);

  TestingProfile* profile2 =
      profile_manager_.CreateTestingProfile("New Profile 2");

  base::RunLoop run_loop;
  app_controller_mac::RunInProfileSafely(
      profile_manager_.profiles_dir().AppendASCII("New Profile 2"),
      base::BindLambdaForTesting([&](Profile* profile) {
        // This should run with the specific profile we asked for, rather than
        // the last-used profile.
        EXPECT_EQ(profile, profile2);
        run_loop.Quit();
      }),
      app_controller_mac::kIgnoreOnFailure);
  run_loop.Run();
}

// Tests that RunInProfileSafely() re-loads the profile from
// disk if it's not currently in memory.
TEST_F(AppControllerSafeProfileTest, SpecificProfileNotLoaded) {
  PrefService* local_state = g_browser_process->local_state();
  local_state->SetString(prefs::kProfileLastUsed,
                         profile_->GetPath().BaseName().MaybeAsASCII());

  AppController* app_controller = AppController.sharedController;
  ASSERT_EQ(profile_, app_controller.lastProfileIfLoaded);

  base::RunLoop run_loop;
  app_controller_mac::RunInProfileSafely(
      profile_manager_.profiles_dir().AppendASCII("New Profile 2"),
      base::BindLambdaForTesting([&](Profile* profile) {
        // This should run with the specific profile we asked for, rather than
        // the last-used profile.
        EXPECT_NE(profile, nullptr);
        EXPECT_NE(profile, profile_.get());
        EXPECT_EQ(profile->GetBaseName().MaybeAsASCII(), "New Profile 2");
        run_loop.Quit();
      }),
      app_controller_mac::kIgnoreOnFailure);
  run_loop.Run();
}
