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

#include "chrome/app_shim/app_shim_controller.h"

#include <inttypes.h>
#include <stdint.h>

#include "base/apple/bundle_locations.h"
#include "base/at_exit.h"
#include "base/base_switches.h"
#include "base/files/file_util.h"
#include "base/path_service.h"
#include "base/strings/string_split.h"
#include "base/strings/stringprintf.h"
#include "base/strings/sys_string_conversions.h"
#include "base/test/bind.h"
#include "base/test/multiprocess_test.h"
#include "base/test/test_timeouts.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/web_applications/test/web_app_browsertest_util.h"
#include "chrome/browser/web_applications/test/os_integration_test_override_impl.h"
#include "chrome/browser/web_applications/test/web_app_install_test_utils.h"
#include "chrome/browser/web_applications/web_app.h"
#include "chrome/browser/web_applications/web_app_filter.h"
#include "chrome/browser/web_applications/web_app_helpers.h"
#include "chrome/browser/web_applications/web_app_provider.h"
#include "chrome/browser/web_applications/web_app_registrar.h"
#include "chrome/common/chrome_paths.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/common/mac/app_mode_common.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "chrome/test/base/launchservices_utils_mac.h"
#include "chrome/test/base/test_launcher_utils.h"
#include "components/remote_cocoa/app_shim/application_bridge.h"
#include "components/remote_cocoa/app_shim/browser_native_widget_window_mac.h"
#include "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#include "components/variations/hashing.h"
#include "components/variations/variations_crash_keys.h"
#include "components/webapps/common/web_app_id.h"
#include "content/public/test/browser_test.h"
#include "net/test/embedded_test_server/embedded_test_server.h"
#include "partition_alloc/buildflags.h"
#include "partition_alloc/partition_address_space.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/multiprocess_func_list.h"
#include "url/gurl.h"

extern "C" {
int APP_SHIM_ENTRY_POINT_NAME(const app_mode::ChromeAppModeInfo* info);
}

#if !defined(OFFICIAL_BUILD)
// This test relies on disabling some code signing checks to work. That is safe
// since we only allow that in unsigned builds, but to be extra secure, the
// relevant code is also only included in unofficial builds. Since most bots
// test with such builds, this still gives us most of the desired test coverage.

namespace web_app {
namespace {
constexpr std::string_view kAppShimPathSwitch = "app-shim-path";
constexpr std::string_view kWebAppIdSwitch = "web-app-id";
constexpr std::string_view kShimLogFileName = "shim.log";

constexpr std::string_view kTrialName = "AppShimTestTrialName";
constexpr std::string_view kTrialGroup1Name = "Group1";
constexpr std::string_view kTrialGroup2Name = "Group2";

std::string GetActiveGroupForTestTrial() {
  std::string trial_name_hash = variations::HashNameAsHexString(kTrialName);
  variations::ExperimentListInfo experiments =
      variations::GetExperimentListInfo();
  for (const std::string& experiment :
       base::SplitString(experiments.experiment_list, ",",
                         base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY)) {
    if (experiment.starts_with(trial_name_hash)) {
      return experiment.substr(trial_name_hash.length() + 1);
    }
  }
  return "";
}

}  // namespace

using AppId = webapps::AppId;

// AppShimController does not live in the browser process, rather it lives in
// an app shim process. Because of this, a regular browser test can not really
// be used to test its implementation. Instead what this browser test does is:
//   - In SetUpOnMainThread (which runs in the browser process), install a test
//     PWA, as setup for the actual test.
//   - Use the "multi process test" support to launch a secondary test process
//     which will act as the app shim process. The actual test body lives in
//     that process.
//   - And currently, the only test specifically tests what happens when Chrome
//     isn't already running. To do this, the test is triggered from SetUp()
//     after InProcessBrowserTest::SetUp() returns. At this point the "test"
//     browser has terminated, allowing us to test this behavior.
class AppShimControllerBrowserTest : public InProcessBrowserTest {
 public:
  AppShimControllerBrowserTest() {
    // Force the chrome that creates the app shim in the first field trial
    // group, so that state gets persisted to the user_data_dir.
    base::FieldTrialList::CreateFieldTrial(kTrialName, kTrialGroup1Name)
        ->Activate();
  }
  ~AppShimControllerBrowserTest() override = default;

  void SetUpOnMainThread() override {
    InProcessBrowserTest::SetUpOnMainThread();
    // Install test PWA, and record the app id and path of generated app shim.
    ASSERT_TRUE(embedded_test_server()->Start());
    const GURL app_url = embedded_test_server()->GetURL("/web_apps/basic.html");
    auto web_app_info =
        WebAppInstallInfo::CreateWithStartUrlForTesting(app_url);
    web_app_info->user_display_mode =
        web_app::mojom::UserDisplayMode::kStandalone;
    app_id_ = web_app::test::InstallWebApp(browser()->GetProfile(),
                                           std::move(web_app_info));
    auto os_integration = OsIntegrationTestOverrideImpl::Get();
    app_shim_path_ = os_integration->GetShortcutPath(
        browser()->GetProfile(), os_integration->chrome_apps_folder(), app_id_,
        "");
    ASSERT_TRUE(!app_shim_path_.empty());
  }

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

    // Now chrome shouldn't be running anymore, so we can do the "real" test.
    base::TimeTicks start_time = base::TimeTicks::Now();

    base::FilePath user_data_dir =
        base::PathService::CheckedGet(chrome::DIR_USER_DATA);

    base::CommandLine command_line(
        base::GetMultiProcessTestChildBaseCommandLine());
    // We need to make sure the child process doesn't initialize NSApp, since we
    // want the app shim code to be able to do that.
    command_line.AppendSwitch(switches::kDoNotCreateNSAppForTests);
    // Pass along various other bits of information the shim process needs.
    command_line.AppendSwitchPath(switches::kUserDataDir, user_data_dir);
    command_line.AppendSwitchPath(kAppShimPathSwitch, app_shim_path_);
    command_line.AppendSwitchASCII(kWebAppIdSwitch, app_id_);

    // Spawn app shim process, and wait for it to exit.
    base::Process test_child_process = base::SpawnMultiProcessTestChild(
        "AppShimControllerBrowserTestAppShimMain", command_line,
        base::LaunchOptions());
    int rv = -1;
    EXPECT_TRUE(base::WaitForMultiprocessTestChildExit(
        test_child_process, TestTimeouts::action_max_timeout(), &rv));
    EXPECT_EQ(0, rv);

    // To validate that the app shim process did what we expected it to do, it
    // writes a log of its actions to disk. Read that file and verify we had
    // the epxected behavior.
    base::FilePath log_file = user_data_dir.AppendASCII(kShimLogFileName);
    std::string log_string;
    base::ReadFileToString(log_file, &log_string);
    std::vector<std::string> log = base::SplitString(
        log_string, "\n", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY);
    const char* expected_brp_log =
#if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
        "BRP Enabled: 1";
#else
        "BRP Enabled: N/A (BRP not supported)";
#endif
    EXPECT_THAT(log, testing::ElementsAre(
                         "Shim Started",
                         base::StringPrintf(
                             "Early Trial Group: %s",
                             variations::HashNameAsHexString(kTrialGroup1Name)),
                         "Window Created: BrowserNativeWidgetWindow",
                         base::StringPrintf(
                             "Final Trial Group: %s",
                             variations::HashNameAsHexString(kTrialGroup2Name)),
                         expected_brp_log,
                         "Window Created: NativeWidgetMacOverlayNSWindow"));

    // If the test failed, it can be hard to debug why without getting output
    // from the Chromium process that was launched by the test. So gather that
    // output from the system log, and include it here.
    if (testing::Test::HasFailure()) {
      base::TimeDelta log_time = base::TimeTicks::Now() - start_time;
      std::vector<std::string> log_argv = {
          "log",
          "show",
          "--process",
          "Chromium",
          "--last",
          base::StringPrintf("%" PRId64 "s", log_time.InSeconds() + 1)};
      std::string log_output;
      base::GetAppOutputAndError(log_argv, &log_output);
      LOG(INFO)
          << "System logs during this test run (could include other tests):\n"
          << log_output;
    }
  }

 protected:
  OsIntegrationTestOverrideBlockingRegistration faked_os_integration_;
  AppId app_id_;
  base::FilePath app_shim_path_;
};

IN_PROC_BROWSER_TEST_F(AppShimControllerBrowserTest, LaunchChrome) {
  // Test body is in SetUp and below in app shim main.
}

class AppShimControllerDelegate : public AppShimController::TestDelegate {
 public:
  AppShimControllerDelegate(base::FunctionRef<void(const std::string&)> log)
      : log_(log) {}

  void PopulateChromeCommandLine(base::CommandLine& command_line) override {
    test_launcher_utils::PrepareBrowserCommandLineForTests(&command_line);
    command_line.AppendSwitch(switches::kAllowAppShimSignatureMismatchForTests);
    log_(base::StringPrintf("Early Trial Group: %s",
                            GetActiveGroupForTestTrial()));

    // Launching chrome with the force field trials flag means that once this
    // shim finished connecting to chrome, its field trial state will have
    // changed so we're now in the different trial group.
    command_line.AppendSwitchASCII(
        switches::kForceFieldTrials,
        base::StringPrintf("*%s/%s", kTrialName, kTrialGroup2Name));
  }

 private:
  base::FunctionRef<void(const std::string&)> log_;
};

MULTIPROCESS_TEST_MAIN(AppShimControllerBrowserTestAppShimMain) {
  base::CommandLine& command_line = *base::CommandLine::ForCurrentProcess();
  base::FilePath user_data_dir =
      command_line.GetSwitchValuePath(switches::kUserDataDir);
  // Shim code expects user data dir to contain 3 extra path components that are
  // subsequentially stripped off again.
  std::string user_data_dir_string = user_data_dir.AppendASCII("profile")
                                         .AppendASCII("Web Applications")
                                         .AppendASCII("appid")
                                         .value();
  std::string app_id = command_line.GetSwitchValueASCII(kWebAppIdSwitch);
  std::string app_shim_path =
      command_line.GetSwitchValueASCII(kAppShimPathSwitch);
  base::apple::SetOverrideMainBundlePath(base::FilePath(app_shim_path));
  std::string framework_path = base::apple::FrameworkBundlePath().value();
  std::string chrome_path = ::test::GuessAppBundlePath().value();

  // Need to reset a bunch of things before we can run the app shim
  // initialization code.
  base::FeatureList::ClearInstanceForTesting();
  base::CommandLine::Reset();
  base::AtExitManager::AllowShadowingForTesting();

  // Log some data to a log file, so the main test process can validate that the
  // test passed.
  base::File log_file(user_data_dir.AppendASCII(kShimLogFileName),
                      base::File::FLAG_WRITE | base::File::FLAG_CREATE);
  auto log = [&log_file](const std::string& s) {
    log_file.WriteAtCurrentPosAndCheck(base::as_byte_span(s + '\n'));
    log_file.Flush();
  };

  AppShimControllerDelegate controller_delegate(log);
  AppShimController::SetDelegateForTesting(&controller_delegate);
  // This test runs as a raw child binary rather than launched from an
  // `.app` bundle via LaunchServices. Even though in-memory bundle
  // lookups are overridden, Apple's UserNotifications framework queries
  // the OS for the running binary, finds no LaunchServices registration,
  // and throws an exception when accessing currentNotificationCenter.
  // Disable creating the notification service specifically for this test.
  AppShimController::SetDisableNotificationServiceForTesting(true);

  log("Shim Started");

  // Close a browser window when it gets created. This should cause chrome and
  // the shim to terminate, marking the end of the test.
  remote_cocoa::ApplicationBridge::Get()->SetNSWindowCreatedCallbackForTesting(
      base::BindLambdaForTesting([&](NativeWidgetMacNSWindow* window) {
        log(base::StringPrintf("Window Created: %s",
                               base::SysNSStringToUTF8([window className])));
        if ([window isKindOfClass:[BrowserNativeWidgetWindow class]]) {
          log(base::StringPrintf("Final Trial Group: %s",
                                 GetActiveGroupForTestTrial()));
#if PA_BUILDFLAG(ENABLE_BACKUP_REF_PTR_SUPPORT)
          void* p = malloc(64);
          bool in_brp = partition_alloc::IsManagedByPartitionAllocBRPPool(
              reinterpret_cast<uintptr_t>(p));
          free(p);
          log(base::StringPrintf("BRP Enabled: %d", in_brp));
#else
          log("BRP Enabled: N/A (BRP not supported)");
#endif
          [window performClose:nil];
        }
      }));

  app_mode::ChromeAppModeInfo info;
  char* argv[] = {};
  info.argc = 0;
  info.argv = argv;
  info.chrome_framework_path = framework_path.c_str();
  info.chrome_outer_bundle_path = chrome_path.c_str();
  info.app_mode_bundle_path = app_shim_path.c_str();
  info.app_mode_id = app_id.c_str();
  info.app_mode_name = "Basic test app";
  info.app_mode_url = "";
  info.profile_dir = "";
  info.user_data_dir = user_data_dir_string.c_str();
  APP_SHIM_ENTRY_POINT_NAME(&info);

  return 0;
}

}  // namespace web_app

#endif  // !defined(OFFICIAL_BUILD)
