<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.

/**
 * Generic moodleforms field.
 *
 * @package    core_form
 * @category   test
 * @copyright  2012 David Monllaó
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.

use Behat\Mink\Element\NodeElement;
use Behat\Mink\Session;

/**
 * Representation of a form field.
 *
 * Basically an interface with Mink session.
 *
 * @package    core_form
 * @category   test
 * @copyright  2012 David Monllaó
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class behat_form_field implements behat_session_interface {

    // All of the functionality of behat_base is shared with form fields via the behat_session_trait trait.
    use behat_session_trait;

    /**
     * @var Session Behat session.
     */
    protected $session;

    /**
     * @var NodeElement The field DOM node to interact with.
     */
    protected $field;

    /**
     * @var string The field's locator.
     */
    protected $fieldlocator = false;

    /**
     * Returns the Mink session.
     *
     * @param   string|null $name name of the session OR active session will be used
     * @return  \Behat\Mink\Session
     */
    public function getSession($name = null) {
        return $this->session;
    }


    /**
     * General constructor with the node and the session to interact with.
     *
     * @param Session $session Reference to Mink session to traverse/modify the page DOM.
     * @param NodeElement $fieldnode The field DOM node
     * @return void
     */
    public function __construct(Session $session, NodeElement $fieldnode) {
        $this->session = $session;
        $this->field = $fieldnode;
    }

    /**
     * Sets the value to a field.
     *
     * @param string $value
     * @return void
     */
    public function set_value($value) {
        // We delegate to the best guess, if we arrived here
        // using the generic behat_form_field is because we are
        // dealing with a fgroup element.
        $instance = $this->guess_type();
        return $instance->set_value($value);
    }

    /**
     * Returns the current value of the select element.
     *
     * @return string
     */
    public function get_value() {
        // We delegate to the best guess, if we arrived here
        // using the generic behat_form_field is because we are
        // dealing with a fgroup element.
        $instance = $this->guess_type();
        return $instance->get_value();
    }

    /**
     * Presses specific keyboard key.
     *
     * @param mixed  $char     could be either char ('b') or char-code (98)
     * @param string $modifier keyboard modifier (could be 'ctrl', 'alt', 'shift' or 'meta')
     */
    public function key_press($char, $modifier = null) {
        // We delegate to the best guess, if we arrived here
        // using the generic behat_form_field is because we are
        // dealing with a fgroup element.
        $instance = $this->guess_type();
        $instance->field->keyDown($char, $modifier);
        try {
            $instance->field->keyPress($char, $modifier);
            $instance->field->keyUp($char, $modifier);
        } catch (\Facebook\WebDriver\Exception\WebDriverException $e) {
            // If the JS handler attached to keydown or keypress destroys the element
            // the later events may trigger errors because form element no longer exist
            // or is not visible. Ignore such exceptions here.
        } catch (\Behat\Mink\Exception\ElementNotFoundException $e) {
            // Other Mink drivers can throw this for the same reason as above.
        }
    }

    /**
     * Generic match implementation
     *
     * Will work well with text-based fields, extension required
     * for most of the other cases.
     *
     * @param string $expectedvalue
     * @return bool The provided value matches the field value?
     */
    public function matches($expectedvalue) {
        // We delegate to the best guess, if we arrived here
        // using the generic behat_form_field is because we are
        // dealing with a fgroup element.
        $instance = $this->guess_type();
        return $instance->matches($expectedvalue);
    }

    /**
     * Get the value of an attribute set on this field.
     *
     * @param string $name The attribute name
     * @return string The attribute value
     */
    public function get_attribute($name) {
        return $this->field->getAttribute($name);
    }

    /**
     * Guesses the element type we are dealing with in case is not a text-based element.
     *
     * This class is the generic field type, behat_field_manager::get_form_field()
     * should be able to find the appropiate class for the field type, but
     * in cases like moodle form group elements we can not find the type of
     * the field through the DOM so we also need to take care of the
     * different field types from here. If we need to deal with more complex
     * moodle form elements we will need to refactor this simple HTML elements
     * guess method.
     *
     * @return behat_form_field
     */
    private function guess_type() {
        return $this->get_field_instance_for_element($this->field);
    }

    /**
     * Returns the appropriate form field object for a given node element.
     *
     * @param NodeElement $element The node element
     * @return behat_form_field
     */
    protected function get_field_instance_for_element(NodeElement $element): behat_form_field {
        global $CFG;

        // We default to the text-based field if nothing was detected.
        if (!$type = behat_field_manager::guess_field_type($element, $this->session)) {
            $type = 'text';
        }

        $classname = 'behat_form_' . $type;
        $classpath = $CFG->dirroot . '/lib/behat/form_field/' . $classname . '.php';
        require_once($classpath);

        return new $classname($this->session, $element);
    }

    /**
     * Returns whether the scenario is running in a browser that can run Javascript or not.
     *
     * @return bool
     */
    protected function running_javascript() {
        return get_class($this->session->getDriver()) !== 'Behat\Mink\Driver\BrowserKitDriver';
    }

    /**
     * Waits for all the JS activity to be completed.
     *
     * @return bool Whether any JS is still pending completion.
     */
    protected function wait_for_pending_js() {
        if (!$this->running_javascript()) {
            // JS is not available therefore there is nothing to wait for.
            return false;
        }

        return behat_base::wait_for_pending_js_in_session($this->session);
    }

    /**
     * Gets the field internal id used by selenium wire protocol.
     *
     * Only available when running_javascript().
     *
     * @throws coding_exception
     * @return int
     */
    protected function get_internal_field_id() {
        if (!$this->running_javascript()) {
            throw new coding_exception('You can only get an internal ID using the selenium driver.');
        }

        return $this->getSession()
            ->getDriver()
            ->getWebDriver()
            ->findElement(WebDriverBy::xpath($node->getXpath()))
            ->getID();
    }

    /**
     * Checks if the provided text matches the field value.
     *
     * @param string $expectedvalue
     * @param string|null $actualvalue The actual value. If not specified, this will be fetched from $this->get_value().
     * @return bool
     */
    protected function text_matches($expectedvalue, ?string $actualvalue = null): bool {
        $actualvalue = $actualvalue ?? $this->get_value();

        // Non strict string comparison.
        if (trim($expectedvalue) == trim($actualvalue)) {
            return true;
        }

        // Do one more matching attempt for floats that are valid with current decsep in use
        // (let's continue non strict comparing them as strings, but once unformatted).
        $expectedfloat = unformat_float(trim($expectedvalue), true);
        $actualfloat = unformat_float(trim($actualvalue), true);
        // If they aren't null or false, then we are good to be compared (basically is_numeric()).
        $goodfloats = !is_null($expectedfloat) && ($expectedfloat !== false) &&
            !is_null($actualfloat) && ($actualfloat !== false);
        if ($goodfloats && ((string)$expectedfloat == (string)$actualfloat)) {
            return true;
        }

        return false;
    }

    /**
     * Gets the field locator.
     *
     * Defaults to the field label but you can
     * specify other locators if you are interested.
     *
     * Public visibility as in most cases will be hard to
     * use this method in a generic way, as fields can
     * be selected using multiple ways (label, id, name...).
     *
     * @throws coding_exception
     * @param string $locatortype
     * @return string
     */
    protected function get_field_locator($locatortype = false) {

        if (!empty($this->fieldlocator)) {
            return $this->fieldlocator;
        }

        $fieldid = $this->field->getAttribute('id');

        // Defaults to label.
        if ($locatortype == 'label' || $locatortype == false) {

            $labelnode = $this->session->getPage()->find('xpath', "//label[@for='$fieldid']|//p[@id='{$fieldid}_label']");

            // Exception only if $locatortype was specified.
            if (!$labelnode && $locatortype == 'label') {
                throw new coding_exception('Field with "' . $fieldid . '" id does not have a label.');
            }

            $this->fieldlocator = $labelnode->getText();
        }

        // Let's look for the name as a second option (more popular than
        // id's when pointing to fields).
        if (($locatortype == 'name' || $locatortype == false) &&
                empty($this->fieldlocator)) {

            $name = $this->field->getAttribute('name');

            // Exception only if $locatortype was specified.
            if (!$name && $locatortype == 'name') {
                throw new coding_exception('Field with "' . $fieldid . '" id does not have a name attribute.');
            }

            $this->fieldlocator = $name;
        }

        // Otherwise returns the id if no specific locator type was provided.
        if (empty($this->fieldlocator)) {
            $this->fieldlocator = $fieldid;
        }

        return $this->fieldlocator;
    }

    /**
     * Returns the field node.
     *
     * @return NodeElement
     */
    public function get_node(): NodeElement {
        return $this->field;
    }
}
