<?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/>.

/**
 * Tests finder
 *
 * @package    core
 * @category   test
 * @copyright  2012 Petr Skoda {@link http://skodak.org}
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

/**
 * Finds components and plugins with tests
 *
 * @package    core
 * @category   test
 * @copyright  2012 Petr Skoda {@link http://skodak.org}
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */
class tests_finder {

    /**
     * Returns all the components with tests of the specified type
     * @param string $testtype The kind of test we are looking for
     * @return array
     */
    public static function get_components_with_tests($testtype) {

        // Get all the components
        $components = self::get_all_plugins_with_tests($testtype) + self::get_all_subsystems_with_tests($testtype);

        // Get all the directories having tests
        $directories = self::get_all_directories_with_tests($testtype);

        // Find any directory not covered by proper components
        $remaining = array_diff($directories, $components);

        // Add them to the list of components
        $components += $remaining;

        return $components;
    }

    /**
     * Returns all the plugins having tests
     * @param string $testtype The kind of test we are looking for
     * @return array  all the plugins having tests
     */
    private static function get_all_plugins_with_tests($testtype) {
        $pluginswithtests = array();

        $plugintypes = core_component::get_plugin_types();
        ksort($plugintypes);
        foreach ($plugintypes as $type => $unused) {
            $plugs = core_component::get_plugin_list($type);
            ksort($plugs);
            foreach ($plugs as $plug => $fullplug) {
                // Look for tests recursively
                if (self::directory_has_tests($fullplug, $testtype)) {
                    $pluginswithtests[$type . '_' . $plug] = $fullplug;
                }
            }
        }
        return $pluginswithtests;
    }

    /**
     * Returns all the subsystems having tests
     *
     * Note we are hacking here the list of subsystems
     * to cover some well-known subsystems that are not properly
     * returned by the {@link get_core_subsystems()} function.
     *
     * @param string $testtype The kind of test we are looking for
     * @return array all the subsystems having tests
     */
    private static function get_all_subsystems_with_tests($testtype) {
        global $CFG;

        $subsystemswithtests = array();

        $subsystems = core_component::get_core_subsystems();

        // Hack the list a bit to cover some well-known ones
        $subsystems['backup'] = $CFG->dirroot.'/backup';
        $subsystems['db-dml'] = $CFG->dirroot.'/lib/dml';
        $subsystems['db-ddl'] = $CFG->dirroot.'/lib/ddl';

        ksort($subsystems);
        foreach ($subsystems as $subsys => $fullsubsys) {
            if ($fullsubsys === null) {
                continue;
            }
            if (!is_dir($fullsubsys)) {
                continue;
            }
            // Look for tests recursively
            if (self::directory_has_tests($fullsubsys, $testtype)) {
                $subsystemswithtests['core_' . $subsys] = $fullsubsys;
            }
        }
        return $subsystemswithtests;
    }

    /**
     * Returns all the directories having tests
     *
     * @param string $testtype The kind of test we are looking for
     * @return array all directories having tests
     */
    private static function get_all_directories_with_tests($testtype) {
        global $CFG;

        // List of directories to exclude from test file searching.
        $excludedir = array('node_modules', 'vendor');

        // Get first level directories in which tests should be searched.
        $directoriestosearch = array();
        $alldirs = glob($CFG->dirroot . DIRECTORY_SEPARATOR . '*' , GLOB_ONLYDIR);
        foreach ($alldirs as $dir) {
            if (!in_array(basename($dir), $excludedir) && (filetype($dir) != 'link')) {
                $directoriestosearch[] = $dir;
            }
        }

        // Search for tests in valid directories.
        $dirs = array();
        foreach ($directoriestosearch as $dir) {
            $dirite = new RecursiveDirectoryIterator($dir);
            $iteite = new RecursiveIteratorIterator($dirite);
            $regexp = self::get_regexp($testtype);
            $regite = new RegexIterator($iteite, $regexp);
            foreach ($regite as $path => $element) {
                $key = dirname(dirname($path));
                $value = trim(str_replace(DIRECTORY_SEPARATOR, '_', str_replace($CFG->dirroot, '', $key)), '_');
                $dirs[$key] = $value;
            }
        }
        ksort($dirs);
        return array_flip($dirs);
    }

    /**
     * Returns if a given directory has tests (recursively)
     *
     * @param string $dir full path to the directory to look for phpunit tests
     * @param string $testtype phpunit|behat
     * @return bool if a given directory has tests (true) or no (false)
     */
    private static function directory_has_tests($dir, $testtype) {
        if (!is_dir($dir)) {
            return false;
        }

        $dirite = new RecursiveDirectoryIterator($dir);
        $iteite = new RecursiveIteratorIterator($dirite);
        $regexp = self::get_regexp($testtype);
        $regite = new RegexIterator($iteite, $regexp);
        $regite->rewind();
        if ($regite->valid()) {
            return true;
        }
        return false;
    }


    /**
     * Returns the regular expression to match by the test files
     * @param string $testtype
     * @return string
     */
    private static function get_regexp($testtype) {

        $sep = preg_quote(DIRECTORY_SEPARATOR, '|');

        switch ($testtype) {
            case 'phpunit':
                $regexp = '|'.$sep.'tests'.$sep.'.*_test\.php$|';
                break;
            case 'features':
                $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'.*\.feature$|';
                break;
            case 'stepsdefinitions':
                $regexp = '|'.$sep.'tests'.$sep.'behat'.$sep.'behat_.*\.php$|';
                break;
            case 'behat':
                $regexp = '!'.$sep.'tests'.$sep.'behat'.$sep.'(.*\.feature)|(behat_.*\.php)$!';
                break;
        }

        return $regexp;
    }
}
