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

#include <utility>

#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/run_loop.h"
#include "components/pref_registry/pref_registry_syncable.h"
#include "components/sync_preferences/testing_pref_service_syncable.h"
#include "extensions/browser/api/runtime/runtime_api.h"
#include "extensions/browser/api_test_utils.h"
#include "extensions/browser/api_unittest.h"
#include "extensions/browser/test_extensions_browser_client.h"
#include "extensions/browser/test_runtime_api_delegate.h"
#include "extensions/buildflags/buildflags.h"
#include "extensions/common/extension_builder.h"

static_assert(BUILDFLAG(ENABLE_EXTENSIONS_CORE));

namespace extensions {

namespace {

// A RuntimeAPIDelegate that simulates a successful restart request every time.
class DelayedRestartTestApiDelegate : public TestRuntimeAPIDelegate {
 public:
  DelayedRestartTestApiDelegate() = default;

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

  ~DelayedRestartTestApiDelegate() override = default;

  // TestRuntimeAPIDelegate:
  bool RestartDevice(std::string* error_message) override {
    if (quit_closure_) {
      std::move(quit_closure_).Run();
    }

    *error_message = "Success.";
    restart_done_ = true;

    return true;
  }

  base::TimeTicks WaitForSuccessfulRestart() {
    if (!restart_done_) {
      base::RunLoop run_loop;
      quit_closure_ = run_loop.QuitClosure();
      run_loop.Run();
    }
    restart_done_ = false;
    return base::TimeTicks::Now();
  }

 private:
  base::OnceClosure quit_closure_;

  bool restart_done_ = false;
};

class DelayedRestartExtensionsBrowserClient
    : public TestExtensionsBrowserClient {
 public:
  explicit DelayedRestartExtensionsBrowserClient(
      content::BrowserContext* context)
      : TestExtensionsBrowserClient(context) {}

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

  ~DelayedRestartExtensionsBrowserClient() override = default;

  std::unique_ptr<RuntimeAPIDelegate> CreateRuntimeAPIDelegate(
      content::BrowserContext* context) const override {
    const_cast<DelayedRestartExtensionsBrowserClient*>(this)->api_delegate_ =
        new DelayedRestartTestApiDelegate();
    return base::WrapUnique(api_delegate_.get());
  }

  DelayedRestartTestApiDelegate* api_delegate() const {
    CHECK(api_delegate_);
    return api_delegate_;
  }

 private:
  raw_ptr<DelayedRestartTestApiDelegate, DanglingUntriaged> api_delegate_ =
      nullptr;  // Not owned.
};

}  // namespace

class RestartAfterDelayApiTest : public ApiUnitTest {
 public:
  RestartAfterDelayApiTest() = default;

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

  ~RestartAfterDelayApiTest() override = default;

  void SetUp() override {
    // Use our ExtensionsBrowserClient that returns our RuntimeAPIDelegate.
    std::unique_ptr<DelayedRestartExtensionsBrowserClient> test_browser_client =
        std::make_unique<DelayedRestartExtensionsBrowserClient>(
            browser_context());

    // ExtensionsTest takes ownership of the ExtensionsBrowserClient.
    SetExtensionsBrowserClient(std::move(test_browser_client));

    ApiUnitTest::SetUp();

    // The RuntimeAPI should only be accessed (i.e. constructed) after the above
    // ExtensionsBrowserClient has been setup.
    RuntimeAPI* runtime_api =
        RuntimeAPI::GetFactoryInstance()->Get(browser_context());
    runtime_api->set_min_duration_between_restarts_for_testing(
        base::Seconds(2));
    runtime_api->AllowNonKioskAppsInRestartAfterDelayForTesting();

    RuntimeAPI::RegisterPrefs(pref_service()->registry());
  }

  base::TimeTicks WaitForSuccessfulRestart() {
    return static_cast<DelayedRestartExtensionsBrowserClient*>(
               extensions_browser_client())
        ->api_delegate()
        ->WaitForSuccessfulRestart();
  }

  bool IsDelayedRestartTimerRunning() {
    return RuntimeAPI::GetFactoryInstance()
        ->Get(browser_context())
        ->restart_after_delay_timer_.IsRunning();
  }

  base::TimeTicks desired_restart_time() {
    return RuntimeAPI::GetFactoryInstance()
        ->Get(browser_context())
        ->restart_after_delay_timer_.desired_run_time();
  }

  void RunRestartAfterDelayFunction(const std::string& args,
                                    const std::string& expected_error) {
    RunRestartAfterDelayFunctionForExtention(args, extension(), expected_error);
  }

  void RunRestartAfterDelayFunctionForExtention(
      const std::string& args,
      const Extension* extension,
      const std::string& expected_error) {
    std::string error = RunFunctionGetError(
        base::MakeRefCounted<RuntimeRestartAfterDelayFunction>(), extension,
        args);
    ASSERT_EQ(error, expected_error);
  }

  void RunRestartFunctionAssertNoError() {
    std::string error = RunFunctionGetError(
        base::MakeRefCounted<RuntimeRestartFunction>(), extension(), "[]");
    ASSERT_TRUE(error.empty()) << error;
  }

 private:
  std::string RunFunctionGetError(scoped_refptr<ExtensionFunction> function,
                                  const Extension* extension,
                                  const std::string& args) {
    scoped_refptr<ExtensionFunction> function_owner(function);
    function->set_extension(extension);
    function->set_has_callback(true);
    api_test_utils::RunFunction(function.get(), args, browser_context());
    return function->GetError();
  }
};

TEST_F(RestartAfterDelayApiTest, RestartAfterDelayTest) {
  RunRestartAfterDelayFunction("[-1]", "");
  ASSERT_FALSE(IsDelayedRestartTimerRunning());

  RunRestartAfterDelayFunction("[-2]", "Invalid argument: -2.");

  // Request a restart after 3 seconds.
  base::TimeTicks now = base::TimeTicks::Now();
  RunRestartAfterDelayFunction("[3]", "");
  ASSERT_TRUE(IsDelayedRestartTimerRunning());
  ASSERT_GE(desired_restart_time() - now, base::Seconds(3));

  // Request another restart after 4 seconds. It should reschedule the previous
  // request.
  now = base::TimeTicks::Now();
  RunRestartAfterDelayFunction("[4]", "");
  ASSERT_TRUE(IsDelayedRestartTimerRunning());
  ASSERT_GE(desired_restart_time() - now, base::Seconds(4));

  // Create another extension and make it attempt to use the api, and expect a
  // failure.
  scoped_refptr<const Extension> test_extension =
      ExtensionBuilder("Another App", ExtensionBuilder::Type::PLATFORM_APP)
          .SetLocation(mojom::ManifestLocation::kInternal)
          .Build();
  RunRestartAfterDelayFunctionForExtention(
      "[5]", test_extension.get(), "Not the first extension to call this API.");

  // Cancel restart requests.
  RunRestartAfterDelayFunction("[-1]", "");
  ASSERT_FALSE(IsDelayedRestartTimerRunning());

  // Schedule a restart and wait for it to happen.
  now = base::TimeTicks::Now();
  RunRestartAfterDelayFunction("[1]", "");
  ASSERT_TRUE(IsDelayedRestartTimerRunning());
  ASSERT_GE(desired_restart_time() - now, base::Seconds(1));
  base::TimeTicks last_restart_time = WaitForSuccessfulRestart();
  ASSERT_FALSE(IsDelayedRestartTimerRunning());
  ASSERT_GE(base::TimeTicks::Now() - now, base::Seconds(1));

  // This is a restart request that will be throttled, because it happens too
  // soon after a successful restart.
  RunRestartAfterDelayFunction(
      "[1]", "Restart was requested too soon. It was throttled instead.");
  ASSERT_TRUE(IsDelayedRestartTimerRunning());
  // Restart will happen 2 seconds later, even though the request was just one
  // second.
  ASSERT_NEAR((desired_restart_time() - last_restart_time).InSecondsF(),
              base::Seconds(2).InSecondsF(), 0.5);

  // Calling chrome.runtime.restart() will not clear the throttle, and any
  // subsequent calls to chrome.runtime.restartAfterDelay will still be
  // throttled.
  WaitForSuccessfulRestart();
  RunRestartFunctionAssertNoError();
  last_restart_time = WaitForSuccessfulRestart();
  RunRestartAfterDelayFunction(
      "[1]", "Restart was requested too soon. It was throttled instead.");
  ASSERT_TRUE(IsDelayedRestartTimerRunning());
  // Restart will happen 2 seconds later, even though the request was just one
  // second.
  ASSERT_NEAR((desired_restart_time() - last_restart_time).InSecondsF(),
              base::Seconds(2).InSecondsF(), 0.5);
}

}  // namespace extensions
