<?php
namespace Codeception\Module;

use Codeception\Step;
use Codeception\TestInterface;
use Facebook\WebDriver\WebDriverBy;

/**
 * Module for AngularJS testing, based on [WebDriver module](http://codeception.com/docs/modules/WebDriver) and [Protractor](http://angular.github.io/protractor/).
 *
 * Performs **synchronization to ensure that page content is fully rendered**.
 * Uses Angular's and Protractor internals methods to synchronize with the page.
 *
 * ## Configuration
 *
 * The same as for [WebDriver](http://codeception.com/docs/modules/WebDriver#Configuration), but few new options added:
 *
 * * `el` - element where Angular application is defined (default: `body`)
 * * `script_timeout` - for how long in seconds to wait for angular operations to finish (default: 5)
 *
 * ### Example (`acceptance.suite.yml`)
 *
 *     modules:
 *        enabled:
 *           - AngularJS:
 *              url: 'http://localhost/'
 *              browser: firefox
 *              script_timeout: 10
 *
 *
 * ### Additional Features
 *
 * Can perform matching elements by model. In this case you should provide a strict locator with `model` set.
 *
 * Example:
 *
 * ```php
 * $I->selectOption(['model' => 'customerId'], '3');
 * ```
 */
class AngularJS extends WebDriver
{
    protected $insideApplication = true;

    protected $defaultAngularConfig = [
        'script_timeout' => 5,
        'el' => 'body',
    ];

    protected $waitForAngular = <<<EOF
  var rootSelector = arguments[0];
  var callback = arguments[1];
  var el = document.querySelector(rootSelector);

  try {
    if (window.getAngularTestability) {
      window.getAngularTestability(el).whenStable(callback);
      return;
    }
    if (!window.angular) {
      throw new Error('window.angular is undefined.  This could be either ' +
          'because this is a non-angular page or because your test involves ' +
          'client-side navigation, which can interfere with Protractor\'s ' +
          'bootstrapping.  See http://git.io/v4gXM for details');
    }
    if (angular.getTestability) {
    angular.getTestability(el).whenStable(callback);
    } else {
      if (!angular.element(el).injector()) {
        throw new Error('root element (' + rootSelector + ') has no injector.' +
           ' this may mean it is not inside ng-app.');
      }
      angular.element(el).injector().get('\$browser').
          notifyWhenNoOutstandingRequests(callback);
    }
  } catch (err) {
    callback(err.message);
  }
EOF;


    public function _setConfig($config)
    {
        parent::_setConfig(array_merge($this->defaultAngularConfig, $config));
    }

    public function _before(TestInterface $test)
    {
        parent::_before($test);
        $this->webDriver->manage()->timeouts()->setScriptTimeout($this->config['script_timeout']);
    }

    /**
     * Enables Angular mode (enabled by default).
     * Waits for Angular to finish rendering after each action.
     */
    public function amInsideAngularApp()
    {
        $this->insideApplication = true;
    }

    /**
     * Disabled Angular mode.
     *
     * Falls back to original WebDriver, in case web page does not contain Angular app.
     */
    public function amOutsideAngularApp()
    {
        $this->insideApplication = false;
    }

    public function _afterStep(Step $step)
    {
        if (!$this->insideApplication) {
            return;
        }
        $actions = [
            'amOnPage',
            'click',
            'fillField',
            'selectOption',
            'checkOption',
            'uncheckOption',
            'unselectOption',
            'doubleClick',
            'appendField',
            'clickWithRightButton',
            'dragAndDrop'
        ];
        if (in_array($step->getAction(), $actions)) {
            $this->webDriver->executeAsyncScript($this->waitForAngular, [$this->config['el']]);
        }
    }

    protected function getStrictLocator(array $by)
    {
        $type = key($by);
        $value = $by[$type];
        if ($type === 'model') {
            return WebDriverBy::cssSelector(sprintf('[ng-model="%s"]', $value));
        }
        return parent::getStrictLocator($by);
    }
}
