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

#include <stddef.h>

#include <list>
#include <vector>

#include "ash/public/cpp/autotest_desks_api.h"
#include "base/command_line.h"
#include "base/functional/callback_helpers.h"
#include "base/strings/string_number_conversions.h"
#include "chrome/browser/ash/crostini/crostini_util.h"
#include "chrome/browser/ash/system_web_apps/system_web_app_manager.h"
#include "chrome/browser/ash/system_web_apps/test_support/system_web_app_browsertest_base.h"
#include "chrome/browser/ash/system_web_apps/test_support/test_system_web_app_installation.h"
#include "chrome/browser/browser_process.h"
#include "chrome/browser/defaults.h"
#include "chrome/browser/prefs/session_startup_pref.h"
#include "chrome/browser/profiles/profile.h"
#include "chrome/browser/sessions/session_restore_test_helper.h"
#include "chrome/browser/ui/ash/system_web_apps/system_web_app_ui_utils.h"
#include "chrome/browser/ui/browser_init_state.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/create_browser_window.h"
#include "chrome/browser/ui/browser_window/public/global_browser_collection.h"
#include "chrome/browser/ui/window_metadata/window_metadata_controller.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/test_launcher_utils.h"
#include "chrome/test/base/ui_test_utils.h"
#include "chromeos/ui/wm/desks/desks_helper.h"
#include "components/prefs/pref_service.h"
#include "components/sessions/core/serialized_navigation_entry_test_helper.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/test_utils.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/gfx/scoped_animation_duration_scale_mode.h"
#include "ui/wm/core/wm_core_switches.h"

namespace {
const char* test_app_name1 = "TestApp1";
const char* test_app_name2 = "TestApp2";

// Activates the desk at |index| and waits for its async operations to complete.
void SwitchToDesk(int index) {
  base::RunLoop run_loop;
  ASSERT_TRUE(ash::AutotestDesksApi().ActivateDeskAtIndex(
      index, run_loop.QuitClosure()));
  run_loop.Run();
}

// Removes all the inactive desks and waits for their async operations to
// complete.
void RemoveInactiveDesks() {
  const int kMaxDeskRemovalTries = 100;
  for (int i = 0; i < kMaxDeskRemovalTries; ++i) {
    base::RunLoop run_loop;
    if (!ash::AutotestDesksApi().RemoveActiveDesk(run_loop.QuitClosure()))
      return;
    run_loop.Run();
  }
  // This should not be reached.
  ADD_FAILURE();
}

}  // namespace

class SessionRestoreTestChromeOS : public InProcessBrowserTest {
 public:
  SessionRestoreTestChromeOS()
      : faster_animations_(
            gfx::ScopedAnimationDurationScaleMode::ZERO_DURATION) {}
  ~SessionRestoreTestChromeOS() override = default;

 protected:
  void SetUpDefaultCommandLine(base::CommandLine* command_line) override {
    InProcessBrowserTest::SetUpDefaultCommandLine(command_line);

    // Animations have caused crashes in session restore in the past but are
    // usually disabled in tests. Remove --wm-window-animations-disabled to
    // re-enable animations.
    command_line->RemoveSwitch(wm::switches::kWindowAnimationsDisabled);
  }

  BrowserWindowInterface* CreateBrowserWithParams(
      BrowserWindowCreateParams params) {
    BrowserWindowInterface* browser = CreateBrowserWindow(std::move(params));
    AddBlankTabAndShow(browser);
    return browser;
  }

  BrowserWindowCreateParams CreateParamsForApp(const std::string& name,
                                               bool trusted) {
    return BrowserWindowCreateParams::CreateForApp(
        name, trusted, gfx::Rect(), profile(), /*user_gesture=*/true);
  }

  BrowserWindowCreateParams CreateParamsForAppPopup(const std::string& name,
                                                    bool trusted) {
    return BrowserWindowCreateParams::CreateForAppPopup(
        name, trusted, gfx::Rect(), profile(), /*user_gesture=*/true);
  }

  // Turn on session restore before we restart.
  void TurnOnSessionRestore() {
    SessionStartupPref::SetStartupPref(
        browser()->GetProfile(), SessionStartupPref(SessionStartupPref::LAST));
  }

  Profile* profile() { return browser()->GetProfile(); }

 private:
  gfx::ScopedAnimationDurationScaleMode faster_animations_;
};

// Thse tests are in pairs. The PRE_ test creates some browser windows and
// the following test confirms that the correct windows are restored after a
// restart.

IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, PRE_RestoreBrowserWindows) {
  // One browser window is always created by default.
  EXPECT_TRUE(browser());
  // Create a second normal browser window.
  CreateBrowserWithParams(
      BrowserWindowCreateParams(profile(), /*from_user_gesture=*/true));
  // Create a third incognito browser window which should not get restored.
  CreateBrowserWithParams(BrowserWindowCreateParams(
      profile()->GetPrimaryOTRProfile(/*create_if_needed=*/true),
      /*from_user_gesture=*/true));
  TurnOnSessionRestore();
}

IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, RestoreBrowserWindows) {
  size_t total_count = 0;
  size_t incognito_count = 0;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&total_count, &incognito_count](BrowserWindowInterface* browser) {
        ++total_count;
        if (browser->GetProfile()->IsOffTheRecord()) {
          ++incognito_count;
        }
        return true;
      });
  EXPECT_EQ(2u, total_count);
  EXPECT_EQ(0u, incognito_count);
}

// Assigns three browser windows to three different desks.
IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS,
                       PRE_RestoreBrowserWindowsToDesks) {
  // Create two more desks so we have three desks in total.
  ash::AutotestDesksApi().CreateNewDesk();
  ash::AutotestDesksApi().CreateNewDesk();

  // A browser window is always created to the current desk, which
  // is the first desk by default.
  EXPECT_TRUE(browser());
  WindowMetadataController::From(browser())->SetWindowUserTitle("0");

  // Create a second normal browser window in the second desk by
  // setting window workspace property.
  SwitchToDesk(1);
  BrowserWindowInterface* browser_desk1 = CreateBrowserWithParams(
      BrowserWindowCreateParams(profile(), /*from_user_gesture=*/true));
  WindowMetadataController::From(browser_desk1)->SetWindowUserTitle("1");
  browser_desk1->GetWindow()->GetNativeWindow()->SetProperty(
      aura::client::kWindowWorkspaceKey, 1);

  // Create a third normal browser window in the third desk
  // specified with params.initial_workspace.
  SwitchToDesk(2);
  BrowserWindowCreateParams browser_desk2_params(profile(),
                                                 /*from_user_gesture=*/true);
  browser_desk2_params.initial_workspace = "2";
  BrowserWindowInterface* browser_desk2 =
      CreateBrowserWithParams(std::move(browser_desk2_params));
  WindowMetadataController::From(browser_desk2)->SetWindowUserTitle("2");

  TurnOnSessionRestore();
}

// Verifies that three windows restored to their right desk after restored. Also
// verifies that the fourth window is visible on all desks after being restored.
IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS,
                       RestoreBrowserWindowsToDesks) {
  ASSERT_EQ(3u, GlobalBrowserCollection::GetInstance()->GetSize());

  // The first, second and third browser should restore to the first, second
  // and third desk, consecutively.
  for (int i = 0; i < 3; i++) {
    // Verify that the browser with title i, has initial_workspace equal to desk
    // i.
    BrowserWindowInterface* const browser =
        ui_test_utils::FindMatchingBrowsers(
            [&](BrowserWindowInterface* browser) {
              int desk_index = 0;
              EXPECT_TRUE(base::StringToInt(
                  BrowserInitState::From(browser)->initial_workspace(),
                  &desk_index));
              return desk_index == i;
            })
            .front();
    ASSERT_TRUE(browser);
    ASSERT_EQ(base::NumberToString(i),
              WindowMetadataController::From(browser)->user_title());

    // Check that a browser window is restored to the right desk i_th.
    ASSERT_TRUE(ash::AutotestDesksApi().IsWindowInDesk(
        browser->GetWindow()->GetNativeWindow(), i));
    int workspace = browser->GetWindow()->GetNativeWindow()->GetProperty(
        aura::client::kWindowWorkspaceKey);
    ASSERT_EQ(i, workspace == aura::client::kWindowWorkspaceUnassignedWorkspace
                     ? 0
                     : workspace);
  }

  RemoveInactiveDesks();
}

// Assigns a browser window to all desks.
IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS,
                       PRE_RestoreAllDesksBrowserWindow) {
  // Create two desks so we have three in total.
  ash::AutotestDesksApi().CreateNewDesk();
  ash::AutotestDesksApi().CreateNewDesk();

  // Create a browser that is visible on all desks.
  BrowserWindowCreateParams visible_on_all_desks_browser_params(
      profile(), /*from_user_gesture=*/true);
  visible_on_all_desks_browser_params.initial_visible_on_all_workspaces_state =
      true;
  BrowserWindowInterface* visible_on_all_desks_browser =
      CreateBrowserWithParams(std::move(visible_on_all_desks_browser_params));

  // Ensure the visible on all desks browser has the right properties.
  auto* visible_on_all_desks_window =
      visible_on_all_desks_browser->GetWindow()->GetNativeWindow();
  ASSERT_TRUE(visible_on_all_desks_window->GetProperty(
                  aura::client::kWindowWorkspaceKey) ==
              aura::client::kWindowWorkspaceVisibleOnAllWorkspaces);
  ASSERT_TRUE(chromeos::DesksHelper::Get()->BelongsToActiveDesk(
      visible_on_all_desks_window));

  // Check that there are two browsers, the default one and the visible on all
  // desks browser.
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  TurnOnSessionRestore();
}

// Verifies that the visible on all desks browser window is restore properly.
IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS,
                       RestoreAllDesksBrowserWindow) {
  // There should be two browsers restored, the default browser and the all
  // desks browser.
  ASSERT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  // Check that the visible on all desks browser is restored properly.
  BrowserWindowInterface* const visible_on_all_desks_browser =
      ui_test_utils::FindMatchingBrowsers([&](BrowserWindowInterface* browser) {
        return browser->GetWindow()->GetNativeWindow()->GetProperty(
                   aura::client::kWindowWorkspaceKey) ==
               aura::client::kWindowWorkspaceVisibleOnAllWorkspaces;
      }).front();
  ASSERT_TRUE(visible_on_all_desks_browser);
  EXPECT_EQ("", BrowserInitState::From(visible_on_all_desks_browser)
                    ->initial_workspace());

  // Visible on all desks windows should always reside on the active desk,
  // even if there is a desk switch.
  aura::Window* const visible_on_all_desks_window =
      visible_on_all_desks_browser->GetWindow()->GetNativeWindow();
  EXPECT_TRUE(chromeos::DesksHelper::Get()->BelongsToActiveDesk(
      visible_on_all_desks_window));

  RemoveInactiveDesks();
}

IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, PRE_RestoreAppsV1) {
  // Create a trusted app.
  CreateBrowserWithParams(CreateParamsForApp(test_app_name1, true));
  // Create a second trusted app with two windows.
  CreateBrowserWithParams(CreateParamsForApp(test_app_name2, true));
  CreateBrowserWithParams(CreateParamsForApp(test_app_name2, true));
  // Create a third untrusted (child) app3. This should not get restored.
  CreateBrowserWithParams(CreateParamsForApp(test_app_name2, false));

  TurnOnSessionRestore();
}

IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, RestoreAppsV1) {
  size_t total_count = 0;
  size_t app1_count = 0;
  size_t app2_count = 0;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&total_count, &app1_count,
       &app2_count](BrowserWindowInterface* browser) {
        ++total_count;
        const std::string& app_name =
            BrowserInitState::From(browser)->create_params().app_name;
        if (app_name == test_app_name1) {
          ++app1_count;
        } else if (app_name == test_app_name2) {
          ++app2_count;
        }
        return true;
      });
  EXPECT_EQ(1u, app1_count);
  EXPECT_EQ(2u, app2_count);   // Only the trusted app windows are restored.
  EXPECT_EQ(4u, total_count);  // Default browser() + 3 app windows
}

IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, PRE_RestoreAppsPopup) {
  // Create a trusted app popup.
  CreateBrowserWithParams(CreateParamsForAppPopup(test_app_name1, true));
  // Create a second trusted app popup with two windows.
  CreateBrowserWithParams(CreateParamsForAppPopup(test_app_name2, true));
  CreateBrowserWithParams(CreateParamsForAppPopup(test_app_name2, true));
  // Create a third untrusted (child) app popup. This should not get restored.
  CreateBrowserWithParams(CreateParamsForAppPopup(test_app_name2, false));

  TurnOnSessionRestore();
}

IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, RestoreAppsPopup) {
  size_t total_count = 0;
  size_t app1_count = 0;
  size_t app2_count = 0;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&total_count, &app1_count,
       &app2_count](BrowserWindowInterface* browser) {
        ++total_count;
        const std::string& app_name =
            BrowserInitState::From(browser)->create_params().app_name;
        if (app_name == test_app_name1) {
          ++app1_count;
        } else if (app_name == test_app_name2) {
          ++app2_count;
        }
        return true;
      });
  EXPECT_EQ(1u, app1_count);
  EXPECT_EQ(2u, app2_count);   // Only the trusted app windows are restored.
  EXPECT_EQ(4u, total_count);  // Default browser() + 3 app windows
}

IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, PRE_RestoreNoDevtools) {
  // Create devtools.
  CreateBrowserWithParams(
      BrowserWindowCreateParams::CreateForDevTools(profile()));

  TurnOnSessionRestore();
}

IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, RestoreNoDevtools) {
  size_t total_count = 0;
  size_t devtools_count = 0;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&total_count, &devtools_count](BrowserWindowInterface* browser) {
        ++total_count;
        if (browser->GetType() == BrowserWindowInterface::TYPE_DEVTOOLS) {
          ++devtools_count;
        }
        return true;
      });
  EXPECT_EQ(1u, total_count);
  EXPECT_EQ(0u, devtools_count);
}

IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, PRE_RestoreMaximized) {
  // One browser window is always created by default.
  ASSERT_TRUE(browser());
  // Create a second browser window and maximize it.
  BrowserWindowInterface* browser2 = CreateBrowserWithParams(
      BrowserWindowCreateParams(profile(), /*from_user_gesture=*/true));
  browser2->GetWindow()->Maximize();

  // Create two app windows and maximize the second one.
  BrowserWindowInterface* app_browser1 =
      CreateBrowserWithParams(CreateParamsForApp(test_app_name1, true));
  BrowserWindowInterface* app_browser2 =
      CreateBrowserWithParams(CreateParamsForApp(test_app_name2, true));
  app_browser2->GetWindow()->Maximize();

  // Create two app popup windows and maximize the second one.
  BrowserWindowInterface* app_popup_browser1 =
      CreateBrowserWithParams(CreateParamsForAppPopup(test_app_name1, true));
  BrowserWindowInterface* app_popup_browser2 =
      CreateBrowserWithParams(CreateParamsForAppPopup(test_app_name2, true));
  app_popup_browser2->GetWindow()->Maximize();

  EXPECT_FALSE(browser()->GetWindow()->IsMaximized());
  EXPECT_TRUE(browser2->GetWindow()->IsMaximized());
  EXPECT_FALSE(app_browser1->GetWindow()->IsMaximized());
  EXPECT_TRUE(app_browser2->GetWindow()->IsMaximized());
  EXPECT_FALSE(app_popup_browser1->GetWindow()->IsMaximized());
  EXPECT_TRUE(app_popup_browser2->GetWindow()->IsMaximized());

  TurnOnSessionRestore();
}

// https://crbug.com/40770352
IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, DISABLED_RestoreMaximized) {
  size_t total_count = 0;
  size_t app1_maximized_count = 0;
  size_t app2_maximized_count = 0;
  size_t total_maximized_count = 0;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&total_count, &app1_maximized_count, &app2_maximized_count,
       &total_maximized_count](BrowserWindowInterface* browser) {
        ++total_count;
        if (browser->GetWindow()->IsMaximized()) {
          ++total_maximized_count;
          const std::string& app_name =
              BrowserInitState::From(browser)->create_params().app_name;
          if (app_name == test_app_name1) {
            ++app1_maximized_count;
          } else if (app_name == test_app_name2) {
            ++app2_maximized_count;
          }
        }
        return true;
      });
  EXPECT_EQ(6u, total_count);
  EXPECT_EQ(0u, app1_maximized_count);
  EXPECT_EQ(2u, app2_maximized_count);  // One TYPE_APP + One TYPE_APP_POPUP
  EXPECT_EQ(3u, total_maximized_count);
}

// Test for crash when restoring minimized windows. http://crbug.com/40500647.
IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, PRE_RestoreMinimized) {
  // One browser window is always created by default.
  ASSERT_TRUE(browser());
  browser()->GetWindow()->Minimize();

  BrowserWindowInterface* browser2 = CreateBrowserWithParams(
      BrowserWindowCreateParams(profile(), /*from_user_gesture=*/true));
  browser2->GetWindow()->Minimize();

  EXPECT_TRUE(browser()->GetWindow()->IsMinimized());
  EXPECT_TRUE(browser2->GetWindow()->IsMinimized());

  TurnOnSessionRestore();
}

// https://crbug.com/40770352
IN_PROC_BROWSER_TEST_F(SessionRestoreTestChromeOS, DISABLED_RestoreMinimized) {
  size_t total_count = 0;
  size_t minimized_count = 0;
  ForEachCurrentBrowserWindowInterfaceOrderedByActivation(
      [&total_count, &minimized_count](BrowserWindowInterface* browser) {
        ++total_count;
        if (browser->GetWindow()->IsMinimized()) {
          ++minimized_count;
        }
        return true;
      });
  EXPECT_EQ(2u, total_count);
  // Chrome OS always activates the last browser windows on login to remind
  // users they have a browser running instead of just showing them an empty
  // desktop.
  EXPECT_NE(2u, minimized_count);
}

class SystemWebAppSessionRestoreTestChromeOS
    : public TestProfileTypeMixin<ash::SystemWebAppBrowserTestBase> {
 public:
  SystemWebAppSessionRestoreTestChromeOS() {
    auto installation =
        ash::TestSystemWebAppInstallation::SetUpStandaloneSingleWindowApp();
    installation->set_update_policy(
        ash::SystemWebAppManager::UpdatePolicy::kOnVersionChange);
    SetSystemWebAppInstallation(std::move(installation));
  }

  ~SystemWebAppSessionRestoreTestChromeOS() override = default;

 protected:
  SessionRestoreTestHelper waiter_;
};

IN_PROC_BROWSER_TEST_P(SystemWebAppSessionRestoreTestChromeOS,
                       PRE_OmitSystemWebApps) {
  // Wait for the app to install, launch, and load, otherwise the app might not
  // be restored.
  WaitForTestSystemAppInstall();
  LaunchApp(GetAppType());

  // Should have one SWA window and one default browser window.
  EXPECT_TRUE(ash::FindSystemWebAppBrowser(
      browser()->GetProfile(), GetAppType(), ash::BrowserType::kApp));
  EXPECT_EQ(2u, GlobalBrowserCollection::GetInstance()->GetSize());

  SessionStartupPref::SetStartupPref(
      browser()->GetProfile(), SessionStartupPref(SessionStartupPref::LAST));
}

IN_PROC_BROWSER_TEST_P(SystemWebAppSessionRestoreTestChromeOS,
                       OmitSystemWebApps) {
  waiter_.Wait();

  // Should have only one default browser window.
  //
  // Session restore doesn't go through system web app launch path, so system
  // web app utils like `FindSystemWebAppBrowser` might not recognize such
  // windows as a SWA browser window. Therefore we count the number of browser
  // windows here instead of trying to find one.
  EXPECT_EQ(1u, GlobalBrowserCollection::GetInstance()->GetSize());
}

INSTANTIATE_SYSTEM_WEB_APP_MANAGER_TEST_SUITE_REGULAR_PROFILE_P(
    SystemWebAppSessionRestoreTestChromeOS);
