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

#include "chrome/test/remoting/remote_desktop_browsertest.h"

#include "base/command_line.h"
#include "base/file_util.h"
#include "base/json/json_reader.h"
#include "base/path_service.h"
#include "chrome/browser/extensions/extension_service.h"
#include "chrome/browser/extensions/unpacked_installer.h"
#include "chrome/browser/ui/extensions/application_launch.h"
#include "chrome/common/chrome_switches.h"
#include "chrome/test/remoting/key_code_conv.h"
#include "chrome/test/remoting/page_load_notification_observer.h"
#include "chrome/test/remoting/waiter.h"
#include "content/public/browser/native_web_keyboard_event.h"
#include "content/public/browser/render_view_host.h"
#include "content/public/test/test_utils.h"
#include "extensions/common/constants.h"
#include "extensions/common/extension.h"
#include "extensions/common/extension_set.h"
#include "extensions/common/switches.h"
#include "ui/base/window_open_disposition.h"

namespace remoting {

RemoteDesktopBrowserTest::RemoteDesktopBrowserTest()
    : extension_(NULL) {
}

RemoteDesktopBrowserTest::~RemoteDesktopBrowserTest() {}

void RemoteDesktopBrowserTest::SetUp() {
  ParseCommandLine();
  PlatformAppBrowserTest::SetUp();
}

void RemoteDesktopBrowserTest::SetUpOnMainThread() {
  PlatformAppBrowserTest::SetUpOnMainThread();

  // Pushing the initial WebContents instance onto the stack before
  // RunTestOnMainThread() and after |InProcessBrowserTest::browser_|
  // is initialized in InProcessBrowserTest::RunTestOnMainThreadLoop()
  web_contents_stack_.push_back(
      browser()->tab_strip_model()->GetActiveWebContents());
}

// Change behavior of the default host resolver to avoid DNS lookup errors,
// so we can make network calls.
void RemoteDesktopBrowserTest::SetUpInProcessBrowserTestFixture() {
  // The resolver object lifetime is managed by sync_test_setup, not here.
  EnableDNSLookupForThisTest(
      new net::RuleBasedHostResolverProc(host_resolver()));
}

void RemoteDesktopBrowserTest::TearDownInProcessBrowserTestFixture() {
  DisableDNSLookupForThisTest();
}

void RemoteDesktopBrowserTest::VerifyInternetAccess() {
  ui_test_utils::NavigateToURLBlockUntilNavigationsComplete(
      browser(), GURL("http://www.google.com"), 1);

  EXPECT_EQ(GetCurrentURL().host(), "www.google.com");
}

void RemoteDesktopBrowserTest::OpenClientBrowserPage() {
  // Open the client browser page in a new tab
  ui_test_utils::NavigateToURLWithDisposition(
      browser(),
      GURL(http_server() + "/clientpage.html"),
      NEW_FOREGROUND_TAB, ui_test_utils::BROWSER_TEST_WAIT_FOR_TAB);

  // Save this web content for later reference
  client_web_content_ = browser()->tab_strip_model()->GetActiveWebContents();

  // Go back to the previous tab that has chromoting opened
  browser()->tab_strip_model()->SelectPreviousTab();
}

bool RemoteDesktopBrowserTest::HtmlElementVisible(const std::string& name) {
  _ASSERT_TRUE(HtmlElementExists(name));

  ExecuteScript(
      "function isElementVisible(name) {"
      "  var element = document.getElementById(name);"
      "  /* The existence of the element has already been ASSERTed. */"
      "  do {"
      "    if (element.hidden) {"
      "      return false;"
      "    }"
      "    element = element.parentNode;"
      "  } while (element != null);"
      "  return true;"
      "};");

  return ExecuteScriptAndExtractBool(
      "isElementVisible(\"" + name + "\")");
}

void RemoteDesktopBrowserTest::InstallChromotingAppCrx() {
  ASSERT_TRUE(!is_unpacked());

  base::FilePath install_dir(WebAppCrxPath());
  scoped_refptr<const Extension> extension(InstallExtensionWithUIAutoConfirm(
      install_dir, 1, browser()));

  EXPECT_FALSE(extension.get() == NULL);

  extension_ = extension.get();
}

void RemoteDesktopBrowserTest::InstallChromotingAppUnpacked() {
  ASSERT_TRUE(is_unpacked());

  scoped_refptr<extensions::UnpackedInstaller> installer =
      extensions::UnpackedInstaller::Create(extension_service());
  installer->set_prompt_for_plugins(false);

  content::WindowedNotificationObserver observer(
      chrome::NOTIFICATION_EXTENSION_LOADED_DEPRECATED,
      content::NotificationService::AllSources());

  installer->Load(webapp_unpacked_);

  observer.Wait();
}

void RemoteDesktopBrowserTest::UninstallChromotingApp() {
  UninstallExtension(ChromotingID());
  extension_ = NULL;
}

void RemoteDesktopBrowserTest::VerifyChromotingLoaded(bool expected) {
  const extensions::ExtensionSet* extensions =
      extension_service()->extensions();
  scoped_refptr<const extensions::Extension> extension;
  bool installed = false;

  for (extensions::ExtensionSet::const_iterator iter = extensions->begin();
       iter != extensions->end(); ++iter) {
    extension = *iter;
    // Is there a better way to recognize the chromoting extension
    // than name comparison?
    if (extension->name() == extension_name_) {
      installed = true;
      break;
    }
  }

  if (installed) {
    if (extension_)
      EXPECT_EQ(extension, extension_);
    else
      extension_ = extension.get();

    // Either a V1 (TYPE_LEGACY_PACKAGED_APP) or a V2 (TYPE_PLATFORM_APP ) app.
    extensions::Manifest::Type type = extension->GetType();
    EXPECT_TRUE(type == extensions::Manifest::TYPE_PLATFORM_APP ||
                type == extensions::Manifest::TYPE_LEGACY_PACKAGED_APP);

    EXPECT_TRUE(extension->ShouldDisplayInAppLauncher());
  }

  ASSERT_EQ(installed, expected);
}

void RemoteDesktopBrowserTest::LaunchChromotingApp() {
  ASSERT_TRUE(extension_);

  GURL chromoting_main = Chromoting_Main_URL();
  // We cannot simply wait for any page load because the first page
  // loaded could be the generated background page. We need to wait
  // till the chromoting main page is loaded.
  PageLoadNotificationObserver observer(chromoting_main);

  OpenApplication(AppLaunchParams(
      browser()->profile(),
      extension_,
      is_platform_app() ? extensions::LAUNCH_CONTAINER_NONE :
          extensions::LAUNCH_CONTAINER_TAB,
      is_platform_app() ? NEW_WINDOW : CURRENT_TAB));

  observer.Wait();


  // The active WebContents instance should be the source of the LOAD_STOP
  // notification.
  content::NavigationController* controller =
      content::Source<content::NavigationController>(observer.source()).ptr();

  content::WebContents* web_contents = controller->GetWebContents();
  if (web_contents != active_web_contents())
    web_contents_stack_.push_back(web_contents);

  app_web_content_ = web_contents;

  if (is_platform_app()) {
    EXPECT_EQ(GetFirstAppWindowWebContents(), active_web_contents());
  } else {
    // For apps v1 only, the DOMOperationObserver is not ready at the LOAD_STOP
    // event. A half second wait is necessary for the subsequent javascript
    // injection to work.
    // TODO(weitaosu): Find out whether there is a more appropriate notification
    // to wait for so we can get rid of this wait.
    ASSERT_TRUE(TimeoutWaiter(base::TimeDelta::FromSeconds(5)).Wait());
  }

  EXPECT_EQ(Chromoting_Main_URL(), GetCurrentURL());
}

void RemoteDesktopBrowserTest::Authorize() {
  // The chromoting extension should be installed.
  ASSERT_TRUE(extension_);

  // The chromoting main page should be loaded in the current tab
  // and isAuthenticated() should be false (auth dialog visible).
  ASSERT_EQ(Chromoting_Main_URL(), GetCurrentURL());
  ASSERT_FALSE(IsAuthenticated());

  // The second observer monitors the loading of the Google login page.
  // Unfortunately we cannot specify a source in this observer because
  // we can't get a handle of the new window until the first observer
  // has finished waiting. But we will assert that the source of the
  // load stop event is indeed the newly created browser window.
  content::WindowedNotificationObserver observer(
      content::NOTIFICATION_LOAD_STOP,
      content::NotificationService::AllSources());

  ClickOnControl("auth-button");

  observer.Wait();

  content::NavigationController* controller =
      content::Source<content::NavigationController>(observer.source()).ptr();

  web_contents_stack_.push_back(controller->GetWebContents());

  // Verify the active tab is at the "Google Accounts" login page.
  EXPECT_EQ("accounts.google.com", GetCurrentURL().host());
  EXPECT_TRUE(HtmlElementExists("Email"));
  EXPECT_TRUE(HtmlElementExists("Passwd"));
}

void RemoteDesktopBrowserTest::Authenticate() {
  // The chromoting extension should be installed.
  ASSERT_TRUE(extension_);

  // The active tab should have the "Google Accounts" login page loaded.
  ASSERT_EQ("accounts.google.com", GetCurrentURL().host());
  ASSERT_TRUE(HtmlElementExists("Email"));
  ASSERT_TRUE(HtmlElementExists("Passwd"));

  // Now log in using the username and password passed in from the command line.
  ExecuteScriptAndWaitForAnyPageLoad(
      "document.getElementById(\"Email\").value = \"" + username_ + "\";" +
      "document.getElementById(\"Passwd\").value = \"" + password_ +"\";" +
      "document.forms[\"gaia_loginform\"].submit();");

  // TODO(weitaosu): Is there a better way to verify we are on the
  // "Request for Permission" page?
  // V2 app won't ask for approval here because the chromoting test account
  // has already been granted permissions.
  if (!is_platform_app()) {
    EXPECT_EQ(GetCurrentURL().host(), "accounts.google.com");
    EXPECT_TRUE(HtmlElementExists("submit_approve_access"));
  }
}

void RemoteDesktopBrowserTest::Approve() {
  // The chromoting extension should be installed.
  ASSERT_TRUE(extension_);

  if (is_platform_app()) {
    // Popping the login window off the stack to return to the chromoting
    // window.
    web_contents_stack_.pop_back();

    // There is nothing for the V2 app to approve because the chromoting test
    // account has already been granted permissions.
    // TODO(weitaosu): Revoke the permission at the beginning of the test so
    // that we can test first-time experience here.
    ConditionalTimeoutWaiter waiter(
        base::TimeDelta::FromSeconds(7),
        base::TimeDelta::FromSeconds(1),
        base::Bind(
            &RemoteDesktopBrowserTest::IsAuthenticatedInWindow,
            active_web_contents()));

    ASSERT_TRUE(waiter.Wait());
  } else {
    ASSERT_EQ("accounts.google.com", GetCurrentURL().host());

    // Is there a better way to verify we are on the "Request for Permission"
    // page?
    ASSERT_TRUE(HtmlElementExists("submit_approve_access"));

    const GURL chromoting_main = Chromoting_Main_URL();

    // active_web_contents() is still the login window but the observer
    // should be set up to observe the chromoting window because that is
    // where we'll receive the message from the login window and reload the
    // chromoting app.
    content::WindowedNotificationObserver observer(
        content::NOTIFICATION_LOAD_STOP,
          base::Bind(
              &RemoteDesktopBrowserTest::IsAuthenticatedInWindow,
              browser()->tab_strip_model()->GetActiveWebContents()));

    ExecuteScript(
        "lso.approveButtonAction();"
        "document.forms[\"connect-approve\"].submit();");

    observer.Wait();

    // Popping the login window off the stack to return to the chromoting
    // window.
    web_contents_stack_.pop_back();
  }

  ASSERT_TRUE(GetCurrentURL() == Chromoting_Main_URL());

  EXPECT_TRUE(IsAuthenticated());
}

void RemoteDesktopBrowserTest::ExpandMe2Me() {
  // The chromoting extension should be installed.
  ASSERT_TRUE(extension_);

  // The active tab should have the chromoting app loaded.
  ASSERT_EQ(Chromoting_Main_URL(), GetCurrentURL());
  EXPECT_TRUE(IsAuthenticated());

  // The Me2Me host list should be hidden.
  ASSERT_FALSE(HtmlElementVisible("me2me-content"));
  // The Me2Me "Get Start" button should be visible.
  ASSERT_TRUE(HtmlElementVisible("get-started-me2me"));

  // Starting Me2Me.
  ExecuteScript("remoting.showMe2MeUiAndSave();");

  EXPECT_TRUE(HtmlElementVisible("me2me-content"));
  EXPECT_FALSE(HtmlElementVisible("me2me-first-run"));

  // Wait until localHost is initialized. This can take a while.
  ConditionalTimeoutWaiter waiter(
      base::TimeDelta::FromSeconds(3),
      base::TimeDelta::FromSeconds(1),
      base::Bind(&RemoteDesktopBrowserTest::IsLocalHostReady, this));
  EXPECT_TRUE(waiter.Wait());

  EXPECT_TRUE(ExecuteScriptAndExtractBool(
      "remoting.hostList.localHost_.hostName && "
      "remoting.hostList.localHost_.hostId && "
      "remoting.hostList.localHost_.status && "
      "remoting.hostList.localHost_.status == 'ONLINE'"));
}

void RemoteDesktopBrowserTest::DisconnectMe2Me() {
  // The chromoting extension should be installed.
  ASSERT_TRUE(extension_);

  // The active tab should have the chromoting app loaded.
  ASSERT_EQ(Chromoting_Main_URL(), GetCurrentURL());
  ASSERT_TRUE(RemoteDesktopBrowserTest::IsSessionConnected());

  ClickOnControl("toolbar-stub");

  EXPECT_TRUE(HtmlElementVisible("session-toolbar"));

  ClickOnControl("toolbar-disconnect");

  EXPECT_TRUE(HtmlElementVisible("client-dialog"));
  EXPECT_TRUE(HtmlElementVisible("client-reconnect-button"));
  EXPECT_TRUE(HtmlElementVisible("client-finished-me2me-button"));

  ClickOnControl("client-finished-me2me-button");

  EXPECT_FALSE(HtmlElementVisible("client-dialog"));
}

void RemoteDesktopBrowserTest::SimulateKeyPressWithCode(
    ui::KeyboardCode keyCode,
    const char* code) {
  SimulateKeyPressWithCode(keyCode, code, false, false, false, false);
}

void RemoteDesktopBrowserTest::SimulateKeyPressWithCode(
    ui::KeyboardCode keyCode,
    const char* code,
    bool control,
    bool shift,
    bool alt,
    bool command) {
  content::SimulateKeyPressWithCode(
      active_web_contents(),
      keyCode,
      code,
      control,
      shift,
      alt,
      command);
}

void RemoteDesktopBrowserTest::SimulateCharInput(char c) {
  const char* code;
  ui::KeyboardCode keyboard_code;
  bool shift;
  GetKeyValuesFromChar(c, &code, &keyboard_code, &shift);
  ASSERT_TRUE(code != NULL);
  SimulateKeyPressWithCode(keyboard_code, code, false, shift, false, false);
}

void RemoteDesktopBrowserTest::SimulateStringInput(const std::string& input) {
  for (size_t i = 0; i < input.length(); ++i)
    SimulateCharInput(input[i]);
}

void RemoteDesktopBrowserTest::SimulateMouseLeftClickAt(int x, int y) {
  SimulateMouseClickAt(0, blink::WebMouseEvent::ButtonLeft, x, y);
}

void RemoteDesktopBrowserTest::SimulateMouseClickAt(
    int modifiers, blink::WebMouseEvent::Button button, int x, int y) {
  // TODO(weitaosu): The coordinate translation doesn't work correctly for
  // apps v2.
  ExecuteScript(
      "var clientPluginElement = "
           "document.getElementById('session-client-plugin');"
      "var clientPluginRect = clientPluginElement.getBoundingClientRect();");

  int top = ExecuteScriptAndExtractInt("clientPluginRect.top");
  int left = ExecuteScriptAndExtractInt("clientPluginRect.left");
  int width = ExecuteScriptAndExtractInt("clientPluginRect.width");
  int height = ExecuteScriptAndExtractInt("clientPluginRect.height");

  ASSERT_GT(x, 0);
  ASSERT_LT(x, width);
  ASSERT_GT(y, 0);
  ASSERT_LT(y, height);

  content::SimulateMouseClickAt(
      browser()->tab_strip_model()->GetActiveWebContents(),
      modifiers,
      button,
      gfx::Point(left + x, top + y));
}

void RemoteDesktopBrowserTest::Install() {
  if (!NoInstall()) {
    VerifyChromotingLoaded(false);
    if (is_unpacked())
      InstallChromotingAppUnpacked();
    else
      InstallChromotingAppCrx();
  }

  VerifyChromotingLoaded(true);
}

void RemoteDesktopBrowserTest::Cleanup() {
  // TODO(weitaosu): Remove this hack by blocking on the appropriate
  // notification.
  // The browser may still be loading images embedded in the webapp. If we
  // uinstall it now those load will fail.
  ASSERT_TRUE(TimeoutWaiter(base::TimeDelta::FromSeconds(2)).Wait());

  if (!NoCleanup()) {
    UninstallChromotingApp();
    VerifyChromotingLoaded(false);
  }

  // TODO(chaitali): Remove this additional timeout after we figure out
  // why this is needed for the v1 app to work.
  // Without this timeout the test fail with a "CloseWebContents called for
  // tab not in our strip" error for the v1 app.
  ASSERT_TRUE(TimeoutWaiter(base::TimeDelta::FromSeconds(2)).Wait());
}

void RemoteDesktopBrowserTest::SetUpTestForMe2Me() {
  VerifyInternetAccess();
  Install();
  LaunchChromotingApp();
  Auth();
  ExpandMe2Me();
  LoadScript(app_web_content(), FILE_PATH_LITERAL("browser_test.js"));
}

void RemoteDesktopBrowserTest::Auth() {
  Authorize();
  Authenticate();
  Approve();
}

void RemoteDesktopBrowserTest::ConnectToLocalHost(bool remember_pin) {
  // Verify that the local host is online.
  ASSERT_TRUE(ExecuteScriptAndExtractBool(
      "remoting.hostList.localHost_.hostName && "
      "remoting.hostList.localHost_.hostId && "
      "remoting.hostList.localHost_.status && "
      "remoting.hostList.localHost_.status == 'ONLINE'"));

  // Connect.
  ClickOnControl("this-host-connect");

  // Enter the pin # passed in from the command line.
  EnterPin(me2me_pin(), remember_pin);

  WaitForConnection();
}

void RemoteDesktopBrowserTest::ConnectToRemoteHost(
    const std::string& host_name, bool remember_pin) {
  std::string host_id = ExecuteScriptAndExtractString(
      "remoting.hostList.getHostIdForName('" + host_name + "')");

  EXPECT_FALSE(host_id.empty());
  std::string element_id = "host_" + host_id;

  // Verify the host is online.
  std::string host_div_class = ExecuteScriptAndExtractString(
      "document.getElementById('" + element_id + "').parentNode.className");
  EXPECT_NE(std::string::npos, host_div_class.find("host-online"));

  ClickOnControl(element_id);

  // Enter the pin # passed in from the command line.
  EnterPin(me2me_pin(), remember_pin);

  WaitForConnection();
}

void RemoteDesktopBrowserTest::EnableDNSLookupForThisTest(
    net::RuleBasedHostResolverProc* host_resolver) {
  // mock_host_resolver_override_ takes ownership of the resolver.
  scoped_refptr<net::RuleBasedHostResolverProc> resolver =
      new net::RuleBasedHostResolverProc(host_resolver);
  resolver->AllowDirectLookup("*.google.com");
  // On Linux, we use Chromium's NSS implementation which uses the following
  // hosts for certificate verification. Without these overrides, running the
  // integration tests on Linux causes errors as we make external DNS lookups.
  resolver->AllowDirectLookup("*.thawte.com");
  resolver->AllowDirectLookup("*.geotrust.com");
  resolver->AllowDirectLookup("*.gstatic.com");
  resolver->AllowDirectLookup("*.googleapis.com");
  mock_host_resolver_override_.reset(
      new net::ScopedDefaultHostResolverProc(resolver.get()));
}

void RemoteDesktopBrowserTest::DisableDNSLookupForThisTest() {
  mock_host_resolver_override_.reset();
}

void RemoteDesktopBrowserTest::ParseCommandLine() {
  CommandLine* command_line = CommandLine::ForCurrentProcess();

  // The test framework overrides any command line user-data-dir
  // argument with a /tmp/.org.chromium.Chromium.XXXXXX directory.
  // That happens in the ChromeTestLauncherDelegate, and affects
  // all unit tests (no opt out available). It intentionally erases
  // any --user-data-dir switch if present and appends a new one.
  // Re-override the default data dir if override-user-data-dir
  // is specified.
  if (command_line->HasSwitch(kOverrideUserDataDir)) {
    const base::FilePath& override_user_data_dir =
        command_line->GetSwitchValuePath(kOverrideUserDataDir);

    ASSERT_FALSE(override_user_data_dir.empty());

    command_line->AppendSwitchPath(switches::kUserDataDir,
                                   override_user_data_dir);
  }

  username_ = command_line->GetSwitchValueASCII(kUsername);
  password_ = command_line->GetSwitchValueASCII(kkPassword);
  me2me_pin_ = command_line->GetSwitchValueASCII(kMe2MePin);
  remote_host_name_ = command_line->GetSwitchValueASCII(kRemoteHostName);
  extension_name_ = command_line->GetSwitchValueASCII(kExtensionName);
  http_server_ = command_line->GetSwitchValueASCII(kHttpServer);

  no_cleanup_ = command_line->HasSwitch(kNoCleanup);
  no_install_ = command_line->HasSwitch(kNoInstall);

  if (!no_install_) {
    webapp_crx_ = command_line->GetSwitchValuePath(kWebAppCrx);
    webapp_unpacked_ = command_line->GetSwitchValuePath(kWebAppUnpacked);
    // One and only one of these two arguments should be provided.
    ASSERT_NE(webapp_crx_.empty(), webapp_unpacked_.empty());
  }

  // Run with "enable-web-based-signin" flag to enforce web-based sign-in,
  // rather than inline signin. This ensures we use the same authentication
  // page, regardless of whether we are testing the v1 or v2 web-app.
  command_line->AppendSwitch(switches::kEnableWebBasedSignin);

  // Enable experimental extensions; this is to allow adding the LG extensions
  command_line->AppendSwitch(
    extensions::switches::kEnableExperimentalExtensionApis);
}

void RemoteDesktopBrowserTest::ExecuteScript(const std::string& script) {
  ASSERT_TRUE(content::ExecuteScript(active_web_contents(), script));
}

void RemoteDesktopBrowserTest::ExecuteScriptAndWaitForAnyPageLoad(
    const std::string& script) {
  content::WindowedNotificationObserver observer(
      content::NOTIFICATION_LOAD_STOP,
      content::Source<content::NavigationController>(
          &active_web_contents()->
              GetController()));

  ExecuteScript(script);

  observer.Wait();
}

// static
bool RemoteDesktopBrowserTest::ExecuteScriptAndExtractBool(
    content::WebContents* web_contents, const std::string& script) {
  bool result;
  EXPECT_TRUE(content::ExecuteScriptAndExtractBool(
      web_contents,
      "window.domAutomationController.send(" + script + ");",
      &result));

  return result;
}

// static
int RemoteDesktopBrowserTest::ExecuteScriptAndExtractInt(
    content::WebContents* web_contents, const std::string& script) {
  int result;
  _ASSERT_TRUE(content::ExecuteScriptAndExtractInt(
      web_contents,
      "window.domAutomationController.send(" + script + ");",
      &result));

  return result;
}

// static
std::string RemoteDesktopBrowserTest::ExecuteScriptAndExtractString(
    content::WebContents* web_contents, const std::string& script) {
  std::string result;
  _ASSERT_TRUE(content::ExecuteScriptAndExtractString(
      web_contents,
      "window.domAutomationController.send(" + script + ");",
      &result));

  return result;
}

// static
bool RemoteDesktopBrowserTest::LoadScript(
    content::WebContents* web_contents,
    const base::FilePath::StringType& path) {
  std::string script;
  base::FilePath src_dir;
  _ASSERT_TRUE(PathService::Get(base::DIR_EXE, &src_dir));
  base::FilePath script_path = src_dir.Append(path);

  if (!base::ReadFileToString(script_path, &script)) {
    LOG(ERROR) << "Failed to load script " << script_path.value();
    return false;
  }

  return content::ExecuteScript(web_contents, script);
}

// static
void RemoteDesktopBrowserTest::RunJavaScriptTest(
    content::WebContents* web_contents,
    const std::string& testName,
    const std::string& testData) {
  std::string result;
  std::string script = "browserTest.runTest(browserTest." + testName + ", " +
                       testData + ");";

  LOG(INFO) << "Executing " << script;

  ASSERT_TRUE(
      content::ExecuteScriptAndExtractString(web_contents, script, &result));

  // Read in the JSON
  base::JSONReader reader;
  scoped_ptr<base::Value> value;
  value.reset(reader.Read(result, base::JSON_ALLOW_TRAILING_COMMAS));

  // Convert to dictionary
  base::DictionaryValue* dict_value = NULL;
  ASSERT_TRUE(value->GetAsDictionary(&dict_value));

  bool succeeded;
  std::string error_message;
  std::string stack_trace;

  // Extract the fields
  ASSERT_TRUE(dict_value->GetBoolean("succeeded", &succeeded));
  ASSERT_TRUE(dict_value->GetString("error_message", &error_message));
  ASSERT_TRUE(dict_value->GetString("stack_trace", &stack_trace));

  EXPECT_TRUE(succeeded) << error_message << "\n" << stack_trace;
}

void RemoteDesktopBrowserTest::ClickOnControl(const std::string& name) {
  ASSERT_TRUE(HtmlElementVisible(name));

  ExecuteScript("document.getElementById(\"" + name + "\").click();");
}

void RemoteDesktopBrowserTest::EnterPin(const std::string& pin,
                                        bool remember_pin) {
  // Wait for the pin-form to be displayed. This can take a while.
  // We also need to dismiss the host-needs-update dialog if it comes up.
  // TODO(weitaosu) 1: Instead of polling, can we register a callback to be
  // called when the pin-form is ready?
  // TODO(weitaosu) 2: Instead of blindly dismiss the host-needs-update dialog,
  // we should verify that it only pops up at the right circumstance. That
  // probably belongs in a separate test case though.
  ConditionalTimeoutWaiter waiter(
      base::TimeDelta::FromSeconds(5),
      base::TimeDelta::FromSeconds(1),
      base::Bind(&RemoteDesktopBrowserTest::IsPinFormVisible, this));
  EXPECT_TRUE(waiter.Wait());

  ExecuteScript(
      "document.getElementById(\"pin-entry\").value = \"" + pin + "\";");

  if (remember_pin) {
    EXPECT_TRUE(HtmlElementVisible("remember-pin"));
    EXPECT_FALSE(ExecuteScriptAndExtractBool(
        "document.getElementById('remember-pin-checkbox').checked"));
    ClickOnControl("remember-pin");
    EXPECT_TRUE(ExecuteScriptAndExtractBool(
        "document.getElementById('remember-pin-checkbox').checked"));
  }

  ClickOnControl("pin-connect-button");
}

void RemoteDesktopBrowserTest::WaitForConnection() {
  // Wait until the client has connected to the server.
  // This can take a while.
  // TODO(weitaosu): Instead of polling, can we register a callback to
  // remoting.clientSession.onStageChange_?
  ConditionalTimeoutWaiter waiter(
      base::TimeDelta::FromSeconds(4),
      base::TimeDelta::FromSeconds(1),
      base::Bind(&RemoteDesktopBrowserTest::IsSessionConnected, this));
  EXPECT_TRUE(waiter.Wait());

  // The client is not yet ready to take input when the session state becomes
  // CONNECTED. Wait for 2 seconds for the client to become ready.
  // TODO(weitaosu): Find a way to detect when the client is truly ready.
  TimeoutWaiter(base::TimeDelta::FromSeconds(2)).Wait();
}

bool RemoteDesktopBrowserTest::IsLocalHostReady() {
  // TODO(weitaosu): Instead of polling, can we register a callback to
  // remoting.hostList.setLocalHost_?
  return ExecuteScriptAndExtractBool("remoting.hostList.localHost_ != null");
}

bool RemoteDesktopBrowserTest::IsSessionConnected() {
  // If some form of PINless authentication is enabled, the host version
  // warning may appear while waiting for the session to connect.
  DismissHostVersionWarningIfVisible();

  return ExecuteScriptAndExtractBool(
      "remoting.clientSession != null && "
      "remoting.clientSession.getState() == "
      "remoting.ClientSession.State.CONNECTED");
}

bool RemoteDesktopBrowserTest::IsPinFormVisible() {
  DismissHostVersionWarningIfVisible();
  return HtmlElementVisible("pin-form");
}

void RemoteDesktopBrowserTest::DismissHostVersionWarningIfVisible() {
  if (HtmlElementVisible("host-needs-update-connect-button"))
    ClickOnControl("host-needs-update-connect-button");
}

// static
bool RemoteDesktopBrowserTest::IsAuthenticatedInWindow(
    content::WebContents* web_contents) {
  return ExecuteScriptAndExtractBool(
      web_contents, "remoting.identity.isAuthenticated()");
}

// static
bool RemoteDesktopBrowserTest::IsHostActionComplete(
    content::WebContents* client_web_content,
    std::string host_action_var) {
  return ExecuteScriptAndExtractBool(
      client_web_content,
      host_action_var);
}

}  // namespace remoting