# Copyright (c) 2012 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.
import json
import logging
import os
import unittest

from telemetry.core import browser_finder
from telemetry.core import exceptions
from telemetry.core import extension_to_load
from telemetry.core import util
from telemetry.core.backends.chrome import cros_interface
from telemetry.unittest import options_for_unittests

class CrOSAutoTest(unittest.TestCase):
  def setUp(self):
    options = options_for_unittests.GetCopy()
    self._cri = cros_interface.CrOSInterface(options.cros_remote,
                                             options.cros_ssh_identity)
    self._is_guest = options.browser_type == 'cros-chrome-guest'
    self._username = '' if self._is_guest else options.browser_options.username
    self._password = options.browser_options.password

  def _IsCryptohomeMounted(self):
    """Returns True if cryptohome is mounted"""
    cryptohomeJSON, _ = self._cri.RunCmdOnDevice(['/usr/sbin/cryptohome',
                                                 '--action=status'])
    cryptohomeStatus = json.loads(cryptohomeJSON)
    return (cryptohomeStatus['mounts'] and
            cryptohomeStatus['mounts'][0]['mounted'])

  def _CreateBrowser(self, autotest_ext=False, auto_login=True):
    """Finds and creates a browser for tests. if autotest_ext is True,
    also loads the autotest extension"""
    options = options_for_unittests.GetCopy()

    if autotest_ext:
      extension_path = os.path.join(os.path.dirname(__file__), 'autotest_ext')
      self._load_extension = extension_to_load.ExtensionToLoad(
          path=extension_path,
          browser_type=options.browser_type,
          is_component=True)
      options.extensions_to_load = [self._load_extension]

    browser_to_create = browser_finder.FindBrowser(options)
    self.assertTrue(browser_to_create)
    options.browser_options.create_browser_with_oobe = True
    options.browser_options.auto_login = auto_login
    b = browser_to_create.Create()
    b.Start()
    return b

  def _GetAutotestExtension(self, browser):
    """Returns the autotest extension instance"""
    extension = browser.extensions[self._load_extension]
    self.assertTrue(extension)
    return extension

  def _GetLoginStatus(self, browser):
      extension = self._GetAutotestExtension(browser)
      self.assertTrue(extension.EvaluateJavaScript(
          "typeof('chrome.autotestPrivate') != 'undefined'"))
      extension.ExecuteJavaScript('''
        window.__login_status = null;
        chrome.autotestPrivate.loginStatus(function(s) {
          window.__login_status = s;
        });
      ''')
      return util.WaitFor(
          lambda: extension.EvaluateJavaScript('window.__login_status'), 10)

  def testCryptohomeMounted(self):
    """Verifies cryptohome mount status for regular and guest user and when
    logged out"""
    with self._CreateBrowser() as b:
      self.assertEquals(1, len(b.tabs))
      self.assertTrue(b.tabs[0].url)
      self.assertTrue(self._IsCryptohomeMounted())

      chronos_fs = self._cri.FilesystemMountedAt('/home/chronos/user')
      self.assertTrue(chronos_fs)
      if self._is_guest:
        self.assertEquals(chronos_fs, 'guestfs')
      else:
        home, _ = self._cri.RunCmdOnDevice(['/usr/sbin/cryptohome-path',
                                            'user', self._username])
        self.assertEquals(self._cri.FilesystemMountedAt(home.rstrip()),
                          chronos_fs)

    self.assertFalse(self._IsCryptohomeMounted())
    self.assertEquals(self._cri.FilesystemMountedAt('/home/chronos/user'),
                      '/dev/mapper/encstateful')

  def testLoginStatus(self):
    """Tests autotestPrivate.loginStatus"""
    with self._CreateBrowser(autotest_ext=True) as b:
      login_status = self._GetLoginStatus(b)
      self.assertEquals(type(login_status), dict)

      self.assertEquals(not self._is_guest, login_status['isRegularUser'])
      self.assertEquals(self._is_guest, login_status['isGuest'])
      self.assertEquals(login_status['email'], self._username)
      self.assertFalse(login_status['isScreenLocked'])

  def _IsScreenLocked(self, browser):
    return self._GetLoginStatus(browser)['isScreenLocked']

  def _LockScreen(self, browser):
      self.assertFalse(self._IsScreenLocked(browser))

      extension = self._GetAutotestExtension(browser)
      self.assertTrue(extension.EvaluateJavaScript(
          "typeof chrome.autotestPrivate.lockScreen == 'function'"))
      logging.info('Locking screen')
      extension.ExecuteJavaScript('chrome.autotestPrivate.lockScreen();')

      logging.info('Waiting for the lock screen')
      def ScreenLocked():
        return (browser.oobe and
            browser.oobe.EvaluateJavaScript("typeof Oobe == 'function'") and
            browser.oobe.EvaluateJavaScript(
            "typeof Oobe.authenticateForTesting == 'function'"))
      util.WaitFor(ScreenLocked, 10)
      self.assertTrue(self._IsScreenLocked(browser))

  def _AttemptUnlockBadPassword(self, browser):
      logging.info('Trying a bad password')
      def ErrorBubbleVisible():
        return not browser.oobe.EvaluateJavaScript('''
            document.getElementById('bubble').hidden
        ''')
      self.assertFalse(ErrorBubbleVisible())
      browser.oobe.ExecuteJavaScript('''
          Oobe.authenticateForTesting('%s', 'bad');
      ''' % self._username)
      util.WaitFor(ErrorBubbleVisible, 10)
      self.assertTrue(self._IsScreenLocked(browser))

  def _UnlockScreen(self, browser):
      logging.info('Unlocking')
      browser.oobe.ExecuteJavaScript('''
          Oobe.authenticateForTesting('%s', '%s');
      ''' % (self._username, self._password))
      util.WaitFor(lambda: not browser.oobe, 10)
      self.assertFalse(self._IsScreenLocked(browser))

  def testScreenLock(self):
    """Tests autotestPrivate.screenLock"""
    with self._CreateBrowser(autotest_ext=True) as browser:
      self._LockScreen(browser)
      self._AttemptUnlockBadPassword(browser)
      self._UnlockScreen(browser)

  def testLogout(self):
    """Tests autotestPrivate.logout"""
    with self._CreateBrowser(autotest_ext=True) as b:
      extension = self._GetAutotestExtension(b)
      try:
        extension.ExecuteJavaScript('chrome.autotestPrivate.logout();')
      except (exceptions.BrowserConnectionGoneException,
              exceptions.BrowserGoneException):
        pass
      util.WaitFor(lambda: not self._IsCryptohomeMounted(), 20)

  def _SwitchRegion(self, region):
    self._cri.RunCmdOnDevice(['stop', 'ui'])

    # Change VPD (requires RW-enabled firmware).
    # To save time, region and initial_timezone are not set.
    vpd = {'initial_locale': region.language_code,
           'keyboard_layout': region.keyboard}

    for (key, value) in vpd.items():
      self._cri.RunCmdOnDevice(['vpd', '-s', '"%s"="%s"' % (key, value)])

    # Remove cached files to clear initial locale info and force regeneration.
    self._cri.RunCmdOnDevice(['rm', '/home/chronos/Local\ State'])
    self._cri.RunCmdOnDevice(['rm', '/home/chronos/.oobe_completed'])
    self._cri.RunCmdOnDevice(['dump_vpd_log', '--force'])

    self._cri.RunCmdOnDevice(['start', 'ui'])

  def _OobeHasOption(self, browser, selectId, value):
    hasOptionJs = '''
      // Check that the option is present, and selected if it is the default.
      (function hasOption(selectId, value, isDefault) {
        var options = document.getElementById(selectId).options;
        for (var i = 0; i < options.length; i++) {
          if (options[i].value == value) {
            // The option is present. Make sure it's selected if necessary.
            return !isDefault || options.selectedIndex == i;
          }
        }
        return false;
      })("%s", "%s", %s);
    '''
    return browser.oobe.EvaluateJavaScript(
        hasOptionJs % (selectId, value, 'true'))

  def _ResolveLanguage(self, locale):
    # If the locale matches a language but not the country, fall back to
    # an existing locale. See ui/base/l10n/l10n_util.cc.
    lang, _, region = map(str.lower, locale.partition('-'))
    if not region:
      return ""

    # Map from other countries to a localized country
    if lang == 'es' and region == 'es':
      return 'es-419'
    if lang == 'zh':
      if region in ('hk', 'mo'):
        return 'zh-TW'
      return 'zh-CN'
    if lang == 'en':
      if region in ('au', 'ca', 'nz', 'za'):
        return 'en-GB'
      return 'en-US'

    # No mapping found
    return ""

  def testOobeLocalization(self):
    """Tests different region configurations at OOBE"""
    # Save the original device localization settings.
    # To save time, only read initial_locale and keyboard_layout.
    initial_region = self.Region('', '', '', '', '')
    initial_region.language_code, _ = self._cri.RunCmdOnDevice(
        ['vpd', '-g', 'initial_locale'])
    initial_region.keyboard, _ = self._cri.RunCmdOnDevice(
        ['vpd', '-g', 'keyboard_layout'])

    for region in self.REGIONS_LIST:
      self._SwitchRegion(region)
      with self._CreateBrowser(auto_login=False) as browser:
        # Ensure the dropdown lists have been created.
        util.WaitFor(lambda: browser.oobe.EvaluateJavaScript(
                     'document.getElementById("language-select") != null'),
                     10)

        # Find the language, or an acceptable fallback value.
        languageFound = self._OobeHasOption(browser,
                                            'language-select',
                                            region.language_code)
        if not languageFound:
          fallback = self._ResolveLanguage(region.language_code)
          self.assertTrue(fallback and
                          self._OobeHasOption(browser,
                                              'language-select',
                                              fallback))

        # Find the keyboard layout.
        self.assertTrue(self._OobeHasOption(
            browser, 'keyboard-select', region.keyboard))

    # Test is finished. Restore original region settings.
    self._SwitchRegion(initial_region)

  # The Region class and region list will be available in regions.py.
  class Region(object):
    def __init__(self, region_code, keyboard, time_zone, language_code,
                 keyboard_mechanical_layout, description=None, notes=None):
      self.region_code = region_code
      self.keyboard = keyboard
      self.time_zone = time_zone
      self.language_code = language_code
      self.keyboard_mechanical_layout = keyboard_mechanical_layout
      self.description = description or region_code
      self.notes = notes

  class Enum(frozenset):
    def __getattr__(self, name):
      if name in self:
        return name
      raise AttributeError

  KeyboardMechanicalLayout = Enum(['ANSI', 'ISO', 'JIS', 'ABNT2'])
  _KML = KeyboardMechanicalLayout
  REGIONS_LIST = [
    Region('au', 'xkb:us::eng', 'Australia/Sydney', 'en-AU', _KML.ANSI,
           'Australia'),
    Region('ca.ansi', 'xkb:us::eng', 'America/Toronto', 'en-CA', _KML.ANSI,
           'Canada (US keyboard)',
           'Canada with US (ANSI) keyboard; see http://goto/cros-canada'),
    Region('ca.fr', 'xkb:ca::fra', 'America/Toronto', 'fr-CA', _KML.ISO,
           'Canada (French keyboard)',
           ('Canadian French (ISO) keyboard. The most common configuration for '
            'Canadian French SKUs.  See http://goto/cros-canada')),
    Region('ca.hybrid', 'xkb:ca:eng:eng', 'America/Toronto', 'en-CA', _KML.ISO,
           'Canada (hybrid)',
           ('Canada with hybrid xkb:ca:eng:eng + xkb:ca::fra keyboard (ISO), '
            'defaulting to English language and keyboard.  Used only if there '
            'needs to be a single SKU for all of Canada.  See '
            'http://goto/cros-canada')),
    Region('ca.multix', 'xkb:ca:multix:fra', 'America/Toronto', 'fr-CA',
           _KML.ISO, 'Canada (multilingual)',
           ("Canadian Multilingual keyboard; you probably don't want this. See "
            "http://goto/cros-canada")),
    Region('de', 'xkb:de::ger', 'Europe/Berlin', 'de', _KML.ISO, 'Germany'),
    Region('fi', 'xkb:fi::fin', 'Europe/Helsinki', 'fi', _KML.ISO, 'Finland'),
    Region('fr', 'xkb:fr::fra', 'Europe/Paris', 'fr', _KML.ISO, 'France'),
    Region('gb', 'xkb:gb:extd:eng', 'Europe/London', 'en-GB', _KML.ISO, 'UK'),
    Region('ie', 'xkb:gb:extd:eng', 'Europe/Dublin', 'en-GB', _KML.ISO,
           'Ireland'),
    Region('in', 'xkb:us::eng', 'Asia/Calcutta', 'en-US', _KML.ANSI, 'India'),
    Region('my', 'xkb:us::eng', 'Asia/Kuala_Lumpur', 'ms', _KML.ANSI,
           'Malaysia'),
    Region('nl', 'xkb:us:intl:eng', 'Europe/Amsterdam', 'nl', _KML.ANSI,
           'Netherlands'),
    Region('nordic', 'xkb:se::swe', 'Europe/Stockholm', 'en-US', _KML.ISO,
           'Nordics',
           ('Unified SKU for Sweden, Norway, and Denmark.  This defaults '
            'to Swedish keyboard layout, but starts with US English language '
            'for neutrality.  Use if there is a single combined SKU for Nordic '
            'countries.')),
    Region('se', 'xkb:se::swe', 'Europe/Stockholm', 'sv', _KML.ISO, 'Sweden',
           ("Use this if there separate SKUs for Nordic countries (Sweden, "
            "Norway, and Denmark), or the device is only shipping to Sweden. "
            "If there is a single unified SKU, use 'nordic' instead.")),
    Region('sg', 'xkb:us::eng', 'Asia/Singapore', 'en-GB', _KML.ANSI,
           'Singapore'),
    Region('us', 'xkb:us::eng', 'America/Los_Angeles', 'en-US', _KML.ANSI,
           'United States'),
  ]

[((18, 14, 18, 45), 'telemetry.unittest.options_for_unittests.GetCopy', 'options_for_unittests.GetCopy', ({}, {}), '()', False, 'from telemetry.unittest import options_for_unittests\n'), ((19, 16, 20, 71), 'telemetry.core.backends.chrome.cros_interface.CrOSInterface', 'cros_interface.CrOSInterface', ({(19, 45, 19, 64): 'options.cros_remote', (20, 45, 20, 70): 'options.cros_ssh_identity'}, {}), '(options.cros_remote, options.cros_ssh_identity)', False, 'from telemetry.core.backends.chrome import cros_interface\n'), ((29, 23, 29, 49), 'json.loads', 'json.loads', ({(29, 34, 29, 48): 'cryptohomeJSON'}, {}), '(cryptohomeJSON)', False, 'import json\n'), ((36, 14, 36, 45), 'telemetry.unittest.options_for_unittests.GetCopy', 'options_for_unittests.GetCopy', ({}, {}), '()', False, 'from telemetry.unittest import options_for_unittests\n'), ((46, 24, 46, 59), 'telemetry.core.browser_finder.FindBrowser', 'browser_finder.FindBrowser', ({(46, 51, 46, 58): 'options'}, {}), '(options)', False, 'from telemetry.core import browser_finder\n'), ((115, 6, 115, 36), 'logging.info', 'logging.info', ({(115, 19, 115, 35): '"""Locking screen"""'}, {}), "('Locking screen')", False, 'import logging\n'), ((118, 6, 118, 49), 'logging.info', 'logging.info', ({(118, 19, 118, 48): '"""Waiting for the lock screen"""'}, {}), "('Waiting for the lock screen')", False, 'import logging\n'), ((124, 6, 124, 36), 'telemetry.core.util.WaitFor', 'util.WaitFor', ({(124, 19, 124, 31): 'ScreenLocked', (124, 33, 124, 35): '(10)'}, {}), '(ScreenLocked, 10)', False, 'from telemetry.core import util\n'), ((128, 6, 128, 43), 'logging.info', 'logging.info', ({(128, 19, 128, 42): '"""Trying a bad password"""'}, {}), "('Trying a bad password')", False, 'import logging\n'), ((137, 6, 137, 42), 'telemetry.core.util.WaitFor', 'util.WaitFor', ({(137, 19, 137, 37): 'ErrorBubbleVisible', (137, 39, 137, 41): '(10)'}, {}), '(ErrorBubbleVisible, 10)', False, 'from telemetry.core import util\n'), ((141, 6, 141, 31), 'logging.info', 'logging.info', ({(141, 19, 141, 30): '"""Unlocking"""'}, {}), "('Unlocking')", False, 'import logging\n'), ((145, 6, 145, 48), 'telemetry.core.util.WaitFor', 'util.WaitFor', ({(145, 19, 145, 43): '(lambda : not browser.oobe)', (145, 45, 145, 47): '(10)'}, {}), '(lambda : not browser.oobe, 10)', False, 'from telemetry.core import util\n'), ((40, 29, 43, 28), 'telemetry.core.extension_to_load.ExtensionToLoad', 'extension_to_load.ExtensionToLoad', (), '', False, 'from telemetry.core import extension_to_load\n'), ((39, 36, 39, 61), 'os.path.dirname', 'os.path.dirname', ({(39, 52, 39, 60): '__file__'}, {}), '(__file__)', False, 'import os\n')]