// Copyright 2023 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 <string>

#import "base/apple/scoped_objc_class_swizzler.h"
#include "chrome/app/chrome_command_ids.h"
#import "chrome/browser/app_controller_mac.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/history/history_service_factory.h"
#include "chrome/browser/lifetime/application_lifetime_desktop.h"
#include "chrome/browser/profiles/profile_attributes_storage.h"
#include "chrome/browser/profiles/profile_test_util.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_element_identifiers.h"
#include "chrome/browser/ui/browser_window.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface.h"
#include "chrome/browser/ui/browser_window/public/browser_window_interface_iterator.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#import "chrome/browser/ui/cocoa/confirm_quit_panel_controller.h"
#include "chrome/browser/ui/cocoa/history_menu_bridge.h"
#include "chrome/browser/ui/profiles/profile_picker.h"
#include "chrome/browser/ui/views/profiles/profile_picker_view.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/pref_names.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/profile_destruction_waiter.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chrome/test/interaction/interactive_browser_test.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "extensions/browser/app_window/app_window_registry.h"
#include "net/base/apple/url_conversions.h"
#include "ui/base/mojom/window_show_state.mojom.h"
#import "ui/events/test/cocoa_test_event_utils.h"
#include "ui/views/test/widget_activation_waiter.h"
#include "ui/views/test/widget_show_state_waiter.h"
#include "ui/views/widget/widget.h"

namespace {

// Instructs the NSApp's delegate to open |url|.
void SendOpenUrlToAppController(const GURL& url) {
  [NSApp.delegate application:NSApp openURLs:@[ net::NSURLWithGURL(url) ]];
}

// Note: These tests interact with SharedController which requires the browser's
// focus. In browser_tests other tests that are running in parallel cause
// flakiness to test test. See: https://crbug.com/40925562

// -------------------AppControllerInteractiveUITest-------------------

using AppControllerInteractiveUITest = InteractiveBrowserTest;

// Regression test for https://crbug.com/40192595
IN_PROC_BROWSER_TEST_F(AppControllerInteractiveUITest, DeleteEphemeralProfile) {
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  Profile* profile = browser()->GetProfile();

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

  // Mark the profile as ephemeral.
  profile->GetPrefs()->SetBoolean(prefs::kForceEphemeralProfiles, true);
  ProfileManager* profile_manager = g_browser_process->profile_manager();
  ProfileAttributesStorage& storage =
      profile_manager->GetProfileAttributesStorage();
  ProfileAttributesEntry* entry =
      storage.GetProfileAttributesWithPath(profile->GetPath());
  EXPECT_TRUE(entry->IsEphemeral());

  // Add sentinel data to observe profile destruction. Ephemeral profiles are
  // destroyed immediately upon browser close.
  ProfileDestructionWaiter waiter(profile);

  // Close browser and wait for the profile to be deleted.
  CloseBrowserSynchronously(browser());
  waiter.Wait();
  EXPECT_EQ(0u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Create a new profile and activate it.
  Profile& profile2 = profiles::testing::CreateProfileSync(
      g_browser_process->profile_manager(),
      profile_manager->user_data_dir().AppendASCII("Profile 2"));
  Browser* browser2 = CreateBrowser(&profile2);
  // This should not crash.
  [[NSNotificationCenter defaultCenter]
      postNotificationName:NSWindowDidBecomeMainNotification
                    object:browser2->GetWindow()
                               ->GetNativeWindow()
                               .GetNativeNSWindow()];
  ASSERT_EQ(&profile2, app_controller.lastProfileIfLoaded);
}

// -------------------AppControllerMainMenuInteractiveUITest-------------------

class AppControllerMainMenuInteractiveUITest : public InProcessBrowserTest {
 protected:
  AppControllerMainMenuInteractiveUITest() = default;
};

// Test switching from Regular to OTR profiles updates the history menu.
IN_PROC_BROWSER_TEST_F(AppControllerMainMenuInteractiveUITest,
                       SwitchToIncognitoRemovesHistoryItems) {
  ASSERT_TRUE(embedded_test_server()->Start());
  AppController* app_controller = AppController.sharedController;

  GURL simple(embedded_test_server()->GetURL("/simple.html"));
  SendOpenUrlToAppController(simple);

  Profile* profile = browser()->GetProfile();
  EXPECT_EQ(GlobalBrowserCollection::GetInstance()->GetSize(), 1u);

  // Load profile's History Service backend so it will be assigned to the
  // HistoryMenuBridge, or else this test will fail flaky.
  ui_test_utils::WaitForHistoryToLoad(HistoryServiceFactory::GetForProfile(
      profile, ServiceAccessType::EXPLICIT_ACCESS));

  // Verify that history bridge service is available for regular profiles.
  EXPECT_TRUE([app_controller historyMenuBridge]->service());
  BrowserWindowInterface* regular_browser =
      GlobalBrowserCollection::GetInstance()->GetLastActiveBrowser();

  // Open a URL in Incognito window.
  ui_test_utils::NavigateToURLWithDisposition(
      browser(), simple, WindowOpenDisposition::OFF_THE_RECORD,
      ui_test_utils::BROWSER_TEST_WAIT_FOR_BROWSER);

  // Check that there are exactly 2 browsers (regular and incognito).
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  BrowserWindowInterface* inc_browser =
      GlobalBrowserCollection::GetInstance()->GetLastActiveBrowser();
  EXPECT_TRUE(inc_browser->GetProfile()->IsIncognitoProfile());

  // Verify that history bridge service is not available in Incognito.
  EXPECT_FALSE([app_controller historyMenuBridge]->service());

  regular_browser->GetWindow()->Show();
  // Verify that history bridge service is available again.
  EXPECT_TRUE([app_controller historyMenuBridge]->service());
}

// Tests opening a new window from dock menu while incognito browser is opened.
// Regression test for https://crbug.com/40241549
IN_PROC_BROWSER_TEST_F(AppControllerMainMenuInteractiveUITest,
                       WhileIncognitoBrowserIsOpened_NewWindow) {
  EXPECT_EQ(GlobalBrowserCollection::GetInstance()->GetSize(), 1u);

  // Create an incognito browser.
  Profile* original_profile = browser()->GetProfile();
  Browser* incognito_browser = CreateIncognitoBrowser(original_profile);
  EXPECT_TRUE(incognito_browser->GetProfile()->IsIncognitoProfile());
  EXPECT_EQ(GlobalBrowserCollection::GetInstance()->GetSize(), 2u);

  // Close the original browser.
  CloseBrowserSynchronously(browser());
  EXPECT_EQ(GlobalBrowserCollection::GetInstance()->GetSize(), 1u);
  EXPECT_EQ(incognito_browser,
            GlobalBrowserCollection::GetInstance()->GetLastActiveBrowser());

  // Simulate click on "New Window".
  ui_test_utils::BrowserCreatedObserver browser_created_observer;
  AppController* app_controller = AppController.sharedController;
  NSMenu* menu = [app_controller applicationDockMenu:NSApp];
  ASSERT_TRUE(menu);
  NSMenuItem* item = [menu itemWithTag:IDC_NEW_WINDOW];
  ASSERT_TRUE(item);
  [app_controller commandDispatch:item];

  // Check that a new non-incognito browser is opened.
  Browser* new_browser = browser_created_observer.Wait();
  EXPECT_EQ(GlobalBrowserCollection::GetInstance()->GetSize(), 2u);
  EXPECT_TRUE(new_browser->GetProfile()->IsRegularProfile());
  EXPECT_EQ(original_profile, new_browser->GetProfile());
}

// Test that when the ProfilePicker is shown, a reopen event focuses the
// ProfilePicker. See crbug.com/429522811.
IN_PROC_BROWSER_TEST_F(AppControllerInteractiveUITest,
                       ProfilePickerReopenFocus) {
  // Activate the Profile Picker.
  ProfilePicker::Show(ProfilePicker::Params::FromEntryPoint(
      ProfilePicker::EntryPoint::kProfileMenuManageProfiles));

  RunTestSequence(
      // Wait for it to be shown and minimize it.
      InAnyContext(WaitForShow(ProfilePickerView::kViewId)),
      InSameContext(Steps(
          Do([]() {
            views::Widget* widget =
                ProfilePicker::GetViewForTesting()->GetWidget();
            widget->Minimize();
            // Wait for it to be fully minimized.
            views::test::WaitForWidgetShowState(
                widget, ui::mojom::WindowShowState::kMinimized);
          }),

          // Close the browser so Picker is the only thing (minimized).
          Do([this]() { browser()->GetWindow()->Close(); }),
          WaitForHide(kBrowserViewElementId), Do([]() {
            EXPECT_EQ(0u, GlobalBrowserCollection::GetInstance()->GetSize());
          }),

          // Simulate Reopen.
          // This should call ProfilePicker::Show() which unminimizes and
          // activates it.
          Do([]() {
            [AppController.sharedController applicationShouldHandleReopen:NSApp
                                                        hasVisibleWindows:YES];
          }),

          // Verify it is visible and active.
          Do([]() {
            views::Widget* widget =
                ProfilePicker::GetViewForTesting()->GetWidget();
            if (!widget->IsActive()) {
              views::test::WaitForWidgetActive(widget, true);
            }
            EXPECT_TRUE(widget->IsVisible());
            EXPECT_TRUE(widget->IsActive());

            // No browser should be opened.
            EXPECT_EQ(0u, GlobalBrowserCollection::GetInstance()->GetSize());
          }))));
}

}  // namespace

@interface AppController (TryToTerminate)
- (void)tryToTerminateApplication;
@end

@interface AppController (TryToTerminateTesting)
- (ConfirmQuitResult)test_confirmQuitIfNeeded;
@end

@implementation AppController (TryToTerminateTesting)
- (ConfirmQuitResult)test_confirmQuitIfNeeded {
  return ConfirmQuitResultAborted;
}
@end

@interface ConfirmQuitPanelController (TryToTerminateTesting)
- (BOOL)test_failConfirmQuitLoopWithEvent:(NSEvent*)event
                        dismissedCallback:(void (^)())dismissedCallback;
@end

@implementation ConfirmQuitPanelController (TryToTerminateTesting)
- (BOOL)test_failConfirmQuitLoopWithEvent:(NSEvent*)event
                        dismissedCallback:(void (^)())dismissedCallback {
  ADD_FAILURE() << "runConfirmQuitLoopWithEvent should not be called";
  return NO;
}
@end

static BOOL g_use_mock_current_event = NO;
static NSEvent* g_mock_current_event = nil;

@interface NSApplication (ConfirmQuitTesting)
- (NSEvent*)test_currentEvent;
@end

@implementation NSApplication (ConfirmQuitTesting)
- (NSEvent*)test_currentEvent {
  if (g_use_mock_current_event) {
    return g_mock_current_event;
  }
  return [self test_currentEvent];
}
@end

namespace {

class ScopedMockCurrentEvent {
 public:
  explicit ScopedMockCurrentEvent(NSEvent* event) {
    g_mock_current_event = event;
    g_use_mock_current_event = YES;
    swizzler_ = std::make_unique<base::apple::ScopedObjCClassSwizzler>(
        [NSApplication class], @selector(currentEvent),
        @selector(test_currentEvent));
  }

  ~ScopedMockCurrentEvent() {
    g_mock_current_event = nil;
    g_use_mock_current_event = NO;
  }

 private:
  std::unique_ptr<base::apple::ScopedObjCClassSwizzler> swizzler_;
};

// -------------------AppControllerTerminateInteractiveUITest-------------------

class AppControllerTerminateInteractiveUITest : public InProcessBrowserTest {
 protected:
  AppControllerTerminateInteractiveUITest() = default;
};

// Verifies that when the Confirm Quit panel is shown and cancelled,
// tryToTerminateApplication correctly aborts the termination sequence.
IN_PROC_BROWSER_TEST_F(AppControllerTerminateInteractiveUITest,
                       ConfirmQuitPanelShownForKeyboardQuit) {
  AppController* app_controller = AppController.sharedController;

  // Set up conditions that should trigger the Confirm Quit panel.
  g_browser_process->local_state()->SetBoolean(prefs::kConfirmToQuitEnabled,
                                               true);
  NSEvent* cmd_q_event = cocoa_test_event_utils::KeyEventWithKeyCode(
      'q', 'q', NSEventTypeKeyDown, NSEventModifierFlagCommand);
  ScopedMockCurrentEvent mock_event(cmd_q_event);

  // Swizzle runConfirmQuitPanel to return NO, which happens if the user does
  // not complete the Confirm Quit action.
  base::apple::ScopedObjCClassSwizzler run_quit_swizzler(
      [AppController class], @selector(confirmQuitIfNeeded),
      @selector(test_confirmQuitIfNeeded));

  // Track closing notifications.
  std::vector<bool> closing_all_browsers_notifications;
  base::CallbackListSubscription subscription =
      chrome::AddClosingAllBrowsersCallback(base::BindRepeating(
          [](std::vector<bool>* notifications, bool closing) {
            notifications->push_back(closing);
          },
          base::Unretained(&closing_all_browsers_notifications)));

  [app_controller tryToTerminateApplication];

  // Verify that no closing notifications were ever sent and that the browser
  // remains open and visible.
  EXPECT_TRUE(closing_all_browsers_notifications.empty());
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
  EXPECT_TRUE(browser()->GetWindow()->IsVisible());
}

// Verifies that when the Confirm Quit panel is bypassed (e.g. mouse quit),
// tryToTerminateApplication initiates the actual closing sequence.
IN_PROC_BROWSER_TEST_F(AppControllerTerminateInteractiveUITest,
                       ConfirmQuitPanelNotShownForMouseQuit) {
  AppController* app_controller = AppController.sharedController;

  // Set up conditions that should bypass the Confirm Quit panel
  g_browser_process->local_state()->SetBoolean(prefs::kConfirmToQuitEnabled,
                                               true);
  ScopedMockCurrentEvent mock_event(nil);

  // Track closing notifications
  std::vector<bool> closing_all_browsers_notifications;
  base::CallbackListSubscription subscription =
      chrome::AddClosingAllBrowsersCallback(base::BindRepeating(
          [](std::vector<bool>* notifications, bool closing) {
            notifications->push_back(closing);
          },
          base::Unretained(&closing_all_browsers_notifications)));

  // Swizzle runConfirmQuitLoopWithEvent to fail if it is ever called.
  base::apple::ScopedObjCClassSwizzler run_quit_swizzler(
      [ConfirmQuitPanelController class],
      @selector(runConfirmQuitLoopWithEvent:dismissedCallback:),
      @selector(test_failConfirmQuitLoopWithEvent:dismissedCallback:));

  [app_controller tryToTerminateApplication];

  // Verify that closing was initiated and NOT cancelled.
  ASSERT_EQ(1u, closing_all_browsers_notifications.size());
  EXPECT_TRUE(closing_all_browsers_notifications[0]);
}

// ---------------AppControllerIncognitoSwitchInteractiveUITest----------------

class AppControllerIncognitoSwitchInteractiveUITest
    : public InProcessBrowserTest {
 public:
  void SetUpCommandLine(base::CommandLine* command_line) override {
    InProcessBrowserTest::SetUpCommandLine(command_line);
    command_line->AppendSwitch(switches::kIncognito);
  }
};

// Regression test for https://crbug.com/40057229
IN_PROC_BROWSER_TEST_F(AppControllerIncognitoSwitchInteractiveUITest,
                       ObserveProfileDestruction) {
  // Chrome is launched in incognito.
  Profile* otr_profile = browser()->GetProfile();
  EXPECT_EQ(otr_profile,
            otr_profile->GetPrimaryOTRProfile(/*create_if_needed=*/false));
  EXPECT_EQ(GlobalBrowserCollection::GetInstance()->GetSize(), 1u);
  AppController* app_controller = AppController.sharedController;

  // The last profile is the incognito profile.
  EXPECT_EQ([app_controller lastProfileIfLoaded], otr_profile);
  // Destroy the incognito profile.
  ProfileDestructionWaiter waiter(otr_profile);
  CloseBrowserSynchronously(browser());
  waiter.Wait();
  // Check that |-lastProfileIfLoaded| is not pointing to released memory.
  EXPECT_NE([app_controller lastProfileIfLoaded], otr_profile);
}

}  // namespace
