<?php
// This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.

namespace tool_generator\local\testscenario;

use behat_data_generators;
use Behat\Gherkin\Parser;
use Behat\Gherkin\Lexer;
use Behat\Gherkin\Keywords\ArrayKeywords;
use ReflectionClass;
use ReflectionMethod;

/**
 * Class to process a scenario generator file.
 *
 * @package    tool_generator
 * @copyright  2023 Ferran Recio <ferran@moodle.com>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class runner {

    /** @var behat_data_generators the behat data generator instance. */
    private behat_data_generators $generator;

    /** @var array of valid steps indexed by given expression tag. */
    private array $validsteps;

    /**
     * Initi all composer, behat libraries and load the valid steps.
     */
    public function init() {
        $this->include_composer_libraries();
        $this->include_behat_libraries();
        $this->load_generator();
    }

    /**
     * Include composer autload.
     */
    public function include_composer_libraries() {
        global $CFG;
        if (!file_exists($CFG->dirroot . '/vendor/autoload.php')) {
            throw new \moodle_exception('Missing composer.');
        }
        require_once($CFG->dirroot . '/vendor/autoload.php');
        return true;
    }

    /**
     * Include all necessary behat libraries.
     */
    public function include_behat_libraries() {
        global $CFG;
        if (!class_exists('Behat\Gherkin\Lexer')) {
            throw new \moodle_exception('Missing behat classes.');
        }

        // Behat constant.
        if (!defined('BEHAT_TEST')) {
            define('BEHAT_TEST', 1);
        }

        // Behat utilities.
        require_once($CFG->libdir . '/behat/classes/util.php');
        require_once($CFG->libdir . '/behat/classes/behat_command.php');
        require_once($CFG->libdir . '/behat/behat_base.php');
        require_once("{$CFG->libdir}/tests/behat/behat_data_generators.php");
        return true;
    }

    /**
     * Load all generators.
     */
    private function load_generator() {
        $this->generator = new behat_data_generators();
        $this->validsteps = $this->scan_generator($this->generator);
    }

    /**
     * Scan a generator to get all valid steps.
     * @param behat_data_generators $generator the generator to scan.
     * @return array the valid steps.
     */
    private function scan_generator(behat_data_generators $generator): array {
        $result = [];
        $class = new ReflectionClass($generator);
        $methods = $class->getMethods(ReflectionMethod::IS_PUBLIC);
        foreach ($methods as $method) {
            $given = $this->get_method_given($method);
            if ($given) {
                $result[$given] = $method->getName();
            }
        }
        return $result;
    }

    /**
     * Get the given expression tag of a method.
     *
     * @param ReflectionMethod $method the method to get the given expression tag.
     * @return string|null the given expression tag or null if not found.
     */
    private function get_method_given(ReflectionMethod $method): ?string {
        $doccomment = $method->getDocComment();
        $doccomment = str_replace("\r\n", "\n", $doccomment);
        $doccomment = str_replace("\r", "\n", $doccomment);
        $doccomment = explode("\n", $doccomment);
        foreach ($doccomment as $line) {
            $matches = [];
            if (preg_match('/.*\@(given|when|then)\s+(.+)$/i', $line, $matches)) {
                return $matches[2];
            }
        }
        return null;
    }

    /**
     * Parse a feature file.
     * @param string $content the feature file content.
     * @return parsedfeature
     */
    public function parse_feature(string $content): parsedfeature {
        $result = new parsedfeature();

        $parser = $this->get_parser();
        $feature = $parser->parse($content);

        // No need for background in testing scenarios because scenarios can only contain generators.
        // In the future the background can be used to define clean up steps (when clean up methods
        // are implemented).
        if ($feature->hasScenarios()) {
            $scenarios = $feature->getScenarios();
            foreach ($scenarios as $scenario) {
                if ($scenario->getNodeType() == 'Outline') {
                    $result->add_scenario($scenario->getNodeType(), $scenario->getTitle());
                    $result->add_error(get_string('testscenario_outline', 'tool_generator'));
                    continue;
                }
                $result->add_scenario($scenario->getNodeType(), $scenario->getTitle());
                $steps = $scenario->getSteps();
                foreach ($steps as $step) {
                    $result->add_step(new steprunner($this->generator, $this->validsteps, $step));
                }
            }
        }
        return $result;
    }

    /**
     * Get the parser.
     * @return Parser
     */
    private function get_parser(): Parser {
        $keywords = new ArrayKeywords([
            'en' => [
                'feature' => 'Feature',
                // If in the future we have clean up steps, background will be renamed to "Clean up".
                'background' => 'Background',
                'scenario' => 'Scenario',
                'scenario_outline' => 'Scenario Outline|Scenario Template',
                'examples' => 'Examples|Scenarios',
                'given' => 'Given',
                'when' => 'When',
                'then' => 'Then',
                'and' => 'And',
                'but' => 'But',
            ],
        ]);
        $lexer = new Lexer($keywords);
        $parser = new Parser($lexer);
        return $parser;
    }

    /**
     * Execute a parsed feature.
     * @param parsedfeature $parsedfeature the parsed feature to execute.
     * @return bool true if all steps were executed successfully.
     */
    public function execute(parsedfeature $parsedfeature): bool {
        if (!$parsedfeature->is_valid()) {
            return false;
        }
        $result = true;
        $steps = $parsedfeature->get_all_steps();
        foreach ($steps as $step) {
            $result = $step->execute() && $result;
        }
        return $result;
    }
}
