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

#include "ash/constants/ash_features.h"
#include "ash/constants/ash_pref_names.h"
#include "base/test/scoped_run_loop_timeout.h"
#include "build/branding_buildflags.h"
#include "build/build_config.h"
#include "chrome/browser/ash/accessibility/accessibility_manager.h"
#include "chrome/browser/ash/accessibility/magnification_manager.h"
#include "chrome/browser/ash/accessibility/magnifier_type.h"
#include "chrome/browser/policy/policy_test_utils.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
#include "chrome/test/base/ui_test_utils.h"
#include "components/policy/core/common/policy_map.h"
#include "components/policy/policy_constants.h"
#include "components/prefs/pref_service.h"
#include "content/public/test/browser_test.h"
#include "content/public/test/browser_test_utils.h"
#include "net/dns/mock_host_resolver.h"
#include "net/test/embedded_test_server/default_handlers.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/ime/fake_text_input_client.h"
#include "ui/base/ime/input_method.h"

namespace policy {
namespace {

#if BUILDFLAG(GOOGLE_CHROME_BRANDING)

ChromeKeyboardControllerClient* GetEnabledKeyboardClient() {
  auto* client = ChromeKeyboardControllerClient::Get();
  if (client != nullptr) {
    client->SetEnableFlag(keyboard::KeyboardEnableFlag::kTouchEnabled);
  }
  return client;
}

ui::InputMethod* GetInputMethod(ChromeKeyboardControllerClient* client) {
  aura::Window* root_window = client->GetKeyboardWindow()->GetRootWindow();
  return root_window != nullptr ? root_window->GetHost()->GetInputMethod()
                                : nullptr;
}

void Wait(base::TimeDelta timeout) {
  base::RunLoop run_loop;
  base::OneShotTimer timer;
  timer.Start(FROM_HERE, timeout, run_loop.QuitClosure());
  run_loop.Run();
  timer.Stop();
}

ui::FakeTextInputClient::Options WebTextFieldOptions() {
  return {
      .type = ui::TEXT_INPUT_TYPE_TEXT,
      .mode = ui::TEXT_INPUT_MODE_TEXT,
      .flags = ui::TEXT_INPUT_FLAG_SPELLCHECK_ON,
  };
}

class KeyboardVisibilityWaiter
    : public ChromeKeyboardControllerClient::Observer {
 public:
  explicit KeyboardVisibilityWaiter(ChromeKeyboardControllerClient* client,
                                    bool visible)
      : visible_(visible) {
    observer_.Observe(client);
  }
  KeyboardVisibilityWaiter(const KeyboardVisibilityWaiter&) = delete;
  KeyboardVisibilityWaiter& operator=(const KeyboardVisibilityWaiter&) = delete;
  ~KeyboardVisibilityWaiter() override = default;

  void Wait() {
    if (observer_.GetSource()->is_keyboard_visible() != visible_) {
      run_loop_.Run();
    }
  }

  // ChromeKeyboardControllerClient::Observer
  void OnKeyboardVisibilityChanged(bool visible) override {
    if (visible == visible_) {
      run_loop_.Quit();
    }
  }

 private:
  bool visible_;
  base::ScopedObservation<ChromeKeyboardControllerClient,
                          ChromeKeyboardControllerClient::Observer>
      observer_{this};
  base::RunLoop run_loop_;
};

void WaitUntilKeyboardShown(ChromeKeyboardControllerClient* client) {
  KeyboardVisibilityWaiter(client, true).Wait();
}

void WaitUntilKeyboardHidden(ChromeKeyboardControllerClient* client) {
  KeyboardVisibilityWaiter(client, false).Wait();
}

#endif

}  // namespace

#if BUILDFLAG(GOOGLE_CHROME_BRANDING)

class VirtualKeyboardPolicyTest : public PolicyTest {};

class VirtualKeyboardPolicyTestWithServer : public PolicyTest {
 public:
  VirtualKeyboardPolicyTestWithServer()
      : https_server_(net::EmbeddedTestServer::TYPE_HTTPS) {}
  ~VirtualKeyboardPolicyTestWithServer() override = default;

  void SetUpOnMainThread() override {
    PolicyTest::SetUpOnMainThread();
    host_resolver()->AddRule("*", "127.0.0.1");
    https_server_.SetSSLConfig(net::EmbeddedTestServer::CERT_TEST_NAMES);
    https_server_.ServeFilesFromSourceDirectory(
        base::FilePath(FILE_PATH_LITERAL("chrome/test/data")));
    content::SetupCrossSiteRedirector(&https_server_);
    net::test_server::RegisterDefaultHandlers(&https_server_);
    ASSERT_TRUE(https_server_.Start());
  }

 protected:
  net::EmbeddedTestServer* https_server() { return &https_server_; }

 private:
  net::EmbeddedTestServer https_server_;
};

IN_PROC_BROWSER_TEST_F(VirtualKeyboardPolicyTest,
                       VirtualKeyboardSmartVisibilityEnabledDefault) {
  auto* keyboard_client = GetEnabledKeyboardClient();
  ASSERT_TRUE(keyboard_client);
  ui::InputMethod* input_method = GetInputMethod(keyboard_client);
  ASSERT_TRUE(input_method);
  ui::FakeTextInputClient text_client(input_method, WebTextFieldOptions());

  // Showing the virtual keyboard, losing focus, then focusing again should show
  // the virtual keyboard again.
  text_client.Focus();
  input_method->SetVirtualKeyboardVisibilityIfEnabled(true);
  WaitUntilKeyboardShown(keyboard_client);
  text_client.Blur();
  WaitUntilKeyboardHidden(keyboard_client);
  text_client.Focus();
  WaitUntilKeyboardShown(keyboard_client);
}

IN_PROC_BROWSER_TEST_F(VirtualKeyboardPolicyTest,
                       VirtualKeyboardSmartVisibilityEnabledEnabled) {
  auto* keyboard_client = GetEnabledKeyboardClient();
  ASSERT_TRUE(keyboard_client);
  ui::InputMethod* input_method = GetInputMethod(keyboard_client);
  ASSERT_TRUE(input_method);
  ui::FakeTextInputClient text_client(input_method, WebTextFieldOptions());

  PolicyMap policies;
  policies.Set(key::kVirtualKeyboardSmartVisibilityEnabled,
               POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
               base::Value(true), nullptr);
  UpdateProviderPolicy(policies);

  // Showing the virtual keyboard, losing focus, then focusing again should show
  // the virtual keyboard again.
  text_client.Focus();
  input_method->SetVirtualKeyboardVisibilityIfEnabled(true);
  WaitUntilKeyboardShown(keyboard_client);
  text_client.Blur();
  WaitUntilKeyboardHidden(keyboard_client);
  text_client.Focus();
  WaitUntilKeyboardShown(keyboard_client);
}

IN_PROC_BROWSER_TEST_F(VirtualKeyboardPolicyTest,
                       VirtualKeyboardSmartVisibilityEnabledDisabled) {
  auto* keyboard_client = GetEnabledKeyboardClient();
  ASSERT_TRUE(keyboard_client);
  ui::InputMethod* input_method = GetInputMethod(keyboard_client);
  ASSERT_TRUE(input_method);
  ui::FakeTextInputClient text_client(input_method, WebTextFieldOptions());

  PolicyMap policies;
  policies.Set(key::kVirtualKeyboardSmartVisibilityEnabled,
               POLICY_LEVEL_MANDATORY, POLICY_SCOPE_USER, POLICY_SOURCE_CLOUD,
               base::Value(false), nullptr);
  UpdateProviderPolicy(policies);

  // Showing the virtual keyboard, losing focus, then focusing again should keep
  // the virtual keyboard hidden.
  text_client.Focus();
  input_method->SetVirtualKeyboardVisibilityIfEnabled(true);
  WaitUntilKeyboardShown(keyboard_client);
  text_client.Blur();
  WaitUntilKeyboardHidden(keyboard_client);
  text_client.Focus();
  Wait(base::Seconds(1));
  EXPECT_FALSE(keyboard_client->is_keyboard_visible());
}

IN_PROC_BROWSER_TEST_F(VirtualKeyboardPolicyTestWithServer,
                       VirtualKeyboardShowAfterCrossOriginNavigation) {
  GURL first_url(
      https_server()->GetURL("a.test", "/virtual_keyboard_policy.html"));
  GURL second_url(https_server()->GetURL("xorigin.a.test",
                                         "/virtual_keyboard_policy.html"));

  auto* keyboard_client = GetEnabledKeyboardClient();
  ASSERT_TRUE(keyboard_client);

  ASSERT_TRUE(ui_test_utils::NavigateToURL(browser(), first_url));

  content::WebContents* web_contents =
      browser()->tab_strip_model()->GetActiveWebContents();

  // Give the frame user activation.
  EXPECT_TRUE(content::ExecJs(web_contents, "() => {}"));
  EXPECT_EQ(true,
            content::EvalJs(web_contents, "navigator.userActivation.isActive",
                            content::EXECUTE_SCRIPT_NO_USER_GESTURE));

  // The cross-origin renderer-initiated navigation will clear user activation.
  ASSERT_TRUE(content::NavigateToURLFromRenderer(web_contents, second_url));
  EXPECT_EQ(false,
            content::EvalJs(web_contents, "navigator.userActivation.isActive",
                            content::EXECUTE_SCRIPT_NO_USER_GESTURE));

  // The virtual keyboard should still show without user activation because the
  // previously navigated page had sticky user activation.
  EXPECT_TRUE(content::ExecJs(web_contents, "navigator.virtualKeyboard.show();",
                              content::EXECUTE_SCRIPT_NO_USER_GESTURE));
  WaitUntilKeyboardShown(keyboard_client);
}
#endif

}  // namespace policy
