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

"""
This module allows tests to interact with the Chrome Web Store (CWS)
using ChromeDriver. They should inherit from the webstore_test class,
and should override the run() method.
"""

import logging
import time

from autotest_lib.client.bin import test
from autotest_lib.client.common_lib import error
from autotest_lib.client.common_lib.cros import chromedriver
from autotest_lib.client.common_lib.global_config import global_config
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions
from selenium.webdriver.support.ui import WebDriverWait

# How long to wait, in seconds, for an app to launch. This is larger
# than it needs to be, because it might be slow on older Chromebooks
_LAUNCH_DELAY = 4

# How long to wait before entering the password when logging in to the CWS
_ENTER_PASSWORD_DELAY = 2

# How long to wait before entering payment info
_PAYMENT_DELAY = 5

def enum(*enumNames):
    """
    Creates an enum. Returns an enum object with a value for each enum
    name, as well as from_string and to_string mappings.

    @param enumNames: The strings representing the values of the enum
    """
    enums = dict(zip(enumNames, range(len(enumNames))))
    reverse = dict((value, key) for key, value in enums.iteritems())
    enums['from_string'] = enums
    enums['to_string'] = reverse
    return type('Enum', (), enums)

# TODO: staging and PNL don't work in these tests (crbug/396660)
TestEnv = enum('staging', 'pnl', 'prod', 'sandbox')

ItemType = enum(
    'hosted_app',
    'packaged_app',
    'chrome_app',
    'extension',
    'theme',
)

# NOTE: paid installs don't work right now
InstallType = enum(
    'free',
    'free_trial',
    'paid',
)

def _labeled_button(label):
    """
    Returns a button with the class webstore-test-button-label and the
    specified label

    @param label: The label on the button
    """
    return ('//div[contains(@class,"webstore-test-button-label") '
            'and text()="' + label + '"]')

def _install_type_click_xpath(item_type, install_type):
    """
    Returns the XPath of the button to install an item of the given type.

    @param item_type: The type of the item to install
    @param install_type: The type of installation being used
    """
    if install_type == InstallType.free:
        return _labeled_button('Free')
    elif install_type == InstallType.free_trial:
        # Both of these cases return buttons that say "Add to Chrome",
        # but they are actually different buttons with only one being
        # visible at a time.
        if item_type == ItemType.hosted_app:
            return ('//div[@id="cxdialog-install-paid-btn" and '
                    '@aria-label="Add to Chrome"]')
        else:
            return _labeled_button('Add to Chrome')
    else:
        return ('//div[contains(@aria-label,"Buy for") '
                'and not(contains(@style,"display: none"))]')

def _get_chrome_flags(test_env):
    """
    Returns the Chrome flags for the given test environment.
    """
    flags = ['--apps-gallery-install-auto-confirm-for-tests=accept']
    if test_env == TestEnv.prod:
        return flags

    url_middle = {
            TestEnv.staging: 'staging.corp',
            TestEnv.sandbox: 'staging.sandbox',
            TestEnv.pnl: 'prod-not-live.corp'
            }[test_env]
    download_url_middle = {
            TestEnv.staging: 'download-staging.corp',
            TestEnv.sandbox: 'download-staging.sandbox',
            TestEnv.pnl: 'omaha.sandbox'
            }[test_env]
    flags.append('--apps-gallery-url=https://webstore-' + url_middle +
            '.google.com')
    flags.append('--apps-gallery-update-url=https://' + download_url_middle +
            '.google.com/service/update2/crx')
    logging.info('Using flags %s', flags)
    return flags


class webstore_test(test.test):
    """
    The base class for tests that interact with the web store.

    Subclasses must define run(), but should not override run_once().
    Subclasses should use methods in this module such as install_item,
    but they can also use the driver directly if they need to.
    """

    def initialize(self, test_env=TestEnv.sandbox,
                   account='cwsbotdeveloper1@gmail.com'):
        """
        Initialize the test.

        @param test_env: The test environment to use
        """
        super(webstore_test, self).initialize()

        self.username = account
        self.password = global_config.get_config_value(
                'CLIENT', 'webstore_test_password', type=str)

        self.test_env = test_env
        self._chrome_flags = _get_chrome_flags(test_env)
        self.webstore_url = {
                TestEnv.staging:
                    'https://webstore-staging.corp.google.com',
                TestEnv.sandbox:
                    'https://webstore-staging.sandbox.google.com/webstore',
                TestEnv.pnl:
                    'https://webstore-prod-not-live.corp.google.com/webstore',
                TestEnv.prod:
                    'https://chrome.google.com/webstore'
                }[test_env]


    def build_url(self, page):
        """
        Builds a webstore URL for the specified page.

        @param page: the page to build a URL for
        """
        return self.webstore_url + page + "?gl=US"


    def detail_page(self, item_id):
        """
        Returns the URL of the detail page for the given item

        @param item_id: The item ID
        """
        return self.build_url("/detail/" + item_id)


    def wait_for(self, xpath):
        """
        Waits until the element specified by the given XPath is visible

        @param xpath: The xpath of the element to wait for
        """
        self._wait.until(expected_conditions.visibility_of_element_located(
                (By.XPATH, xpath)))


    def run_once(self, **kwargs):
        with chromedriver.chromedriver(
                username=self.username,
                password=self.password,
                extra_chrome_flags=self._chrome_flags) \
                as chromedriver_instance:
            self.driver = chromedriver_instance.driver
            self.driver.implicitly_wait(15)
            self._wait = WebDriverWait(self.driver, 20)
            logging.info('Running test on test environment %s',
                    TestEnv.to_string[self.test_env])
            self.run(**kwargs)


    def run(self):
        """
        Runs the test. Should be overridden by subclasses.
        """
        raise error.TestError('The test needs to override run()')


    def install_item(self, item_id, item_type, install_type):
        """
        Installs an item from the CWS.

        @param item_id: The ID of the item to install
                (a 32-char string of letters)
        @param item_type: The type of the item to install
        @param install_type: The type of installation
                (free, free trial, or paid)
        """
        logging.info('Installing item %s of type %s with install_type %s',
                item_id, ItemType.to_string[item_type],
                InstallType.to_string[install_type])

        # We need to go to the CWS home page before going to the detail
        # page due to a bug in the CWS
        self.driver.get(self.webstore_url)
        self.driver.get(self.detail_page(item_id))

        install_type_click_xpath = _install_type_click_xpath(
                item_type, install_type)
        if item_type == ItemType.extension or item_type == ItemType.theme:
            post_install_xpath = (
                '//div[@aria-label="Added to Chrome" '
                ' and not(contains(@style,"display: none"))]')
        else:
            post_install_xpath = _labeled_button('Launch app')

        # In this case we need to sign in again
        if install_type != InstallType.free:
            button_xpath = _labeled_button('Sign in to add')
            logging.info('Clicking button %s', button_xpath)
            self.driver.find_element_by_xpath(button_xpath).click()
            time.sleep(_ENTER_PASSWORD_DELAY)
            password_field = self.driver.find_element_by_xpath(
                    '//input[@id="Passwd"]')
            password_field.send_keys(self.password)
            self.driver.find_element_by_xpath('//input[@id="signIn"]').click()

        logging.info('Clicking %s', install_type_click_xpath)
        self.driver.find_element_by_xpath(install_type_click_xpath).click()

        if install_type == InstallType.paid:
            handle = self.driver.current_window_handle
            iframe = self.driver.find_element_by_xpath(
                '//iframe[contains(@src, "sandbox.google.com/checkout")]')
            self.driver.switch_to_frame(iframe)
            self.driver.find_element_by_id('purchaseButton').click()
            time.sleep(_PAYMENT_DELAY) # Wait for animation to finish
            self.driver.find_element_by_id('finishButton').click()
            self.driver.switch_to_window(handle)

        self.wait_for(post_install_xpath)


    def launch_app(self, app_id):
        """
        Launches an app. Verifies that it launched by verifying that
        a new tab/window was opened.

        @param app_id: The ID of the app to run
        """
        logging.info('Launching app %s', app_id)
        num_handles_before = len(self.driver.window_handles)
        self.driver.get(self.webstore_url)
        self.driver.get(self.detail_page(app_id))
        launch_button = self.driver.find_element_by_xpath(
            _labeled_button('Launch app'))
        launch_button.click();
        time.sleep(_LAUNCH_DELAY) # Wait for the app to launch
        num_handles_after = len(self.driver.window_handles)
        if num_handles_after <= num_handles_before:
            raise error.TestError('App failed to launch')