#!/usr/bin/php -d xdebug.mode=coverage,profile
<?php
extension_loaded('xdebug') or die('The XDebug extension must be installed.');
ini_set('display_errors', 1);
error_reporting(E_ALL);
set_time_limit(0);
ini_set('max_execution_time',0);
defined('XDEBUG_FILTER_CODE_COVERAGE') or define('XDEBUG_FILTER_CODE_COVERAGE',0b100000000);
defined('XDEBUG_PATH_INCLUDE') or define('XDEBUG_PATH_INCLUDE',0b000000001);
defined('XDEBUG_PATH_EXCLUDE') or define('XDEBUG_PATH_EXCLUDE',0b000000010);
defined('XDEBUG_CC_UNUSED') or define('XDEBUG_CC_UNUSED',0b000000001);
defined('XDEBUG_CC_DEAD_CODE') or define('XDEBUG_CC_DEAD_CODE',0b000000010);
defined('XDEBUG_CC_BRANCH_CHECK') or define('XDEBUG_CC_BRANCH_CHECK',0b000000100);
defined('XDEBUG_TRACE_HTML') or define('XDEBUG_TRACE_HTML',0b000000100);

if (file_exists('./config/host.php')):
    require_once './config/host.php';
endif;

defined('INST_PATH') || define('INST_PATH', dirname(realpath('./')).'/');
set_include_path(
    '/etc/dumbophp'.PATH_SEPARATOR.
    '/etc/dumbophp/bin'.PATH_SEPARATOR.
    INST_PATH.'vendor'.PATH_SEPARATOR.
    INST_PATH.'vendor/rantes/dumbophp'.PATH_SEPARATOR.
    INST_PATH.'vendor/rantes/dumbophp/bin'.PATH_SEPARATOR.
    INST_PATH.PATH_SEPARATOR.
    get_include_path().PATH_SEPARATOR.
    PEAR_EXTENSION_DIR.PATH_SEPARATOR.
    '/windows/dumbophp'.PATH_SEPARATOR.
    '/windows/dumbophp/bin'.PATH_SEPARATOR.
    '/windows/system32/dumbophp'.PATH_SEPARATOR.
    '/windows/system32/dumbophp/bin'.PATH_SEPARATOR.
    INST_PATH.'DumboPHP'
);

require_once 'lib/DumboShellColors.php';
require_once 'dumbophp.php';
require_once 'lib/Timothy/dumboTests.php';
require_once 'lib/Timothy/testDispatcher.php';

class dumboShell{
    private $_options = [
        'env' => ['value' => null, 'cast' => 'string'],
        'halt' => ['value' => false, 'cast' => 'boolean'],
        'dir' => ['value' => null, 'cast' => 'string'],
        'watch' => ['value' => false, 'cast' => 'boolean']
    ];
    private $arguments = [];
    private $params = [];
    private $colors = null;
    private $_failed = false;
    public $shellOutput = true;

    public function __construct() {
        $GLOBALS['env'] = 'test';
        $this->colors = new DumboShellColors();
    }

    private function _logger($message, $out = STDOUT, $source = 'unit_testing') {
        if (empty($source) or empty($message) or !is_string($source) or !is_string($message)):
            return false;
        endif;
        $logdir = INST_PATH.'tmp/logs/';
        is_dir($logdir) or mkdir($logdir, 0777, true);
        $file = "{$source}.log";
        $stamp = date('d-m-Y i:s:H');

        file_exists("{$logdir}{$file}") and filesize("{$logdir}{$file}") >= 524288000 and rename("{$logdir}{$file}", "{$logdir}{$stamp}_{$file}");
        $this->shellOutput and fwrite($out, "{$message}\n");
        file_put_contents("{$logdir}{$file}", "[{$stamp}] - {$message}\n", FILE_APPEND);
        return true;
    }

    private function _parseOptions() {
        $trueFalse = ['true' => true, 'false' => false];
        foreach($this->arguments as $i => $arg):
            preg_match('@\-\-([a-zA-Z0-9]+)\=([a-z0-9\-\_\/]+)[\s]*@im', $arg, $match);
            if (sizeof($match) === 3):
                if(isset($this->_options[$match[1]])):
                    switch($this->_options[$match[1]]['cast']):
                        case 'numeric':
                            $match[2] = (integer)$match[2];
                        break;
                        case 'boolean':
                            $match[2] = $trueFalse[strtolower($match[2])];
                        break;
                        case 'string':
                            $match[2] = trim((string)$match[2]);
                        break;
                        default:
                            throw new Exception("Value not allowed for {$match[1]}");
                    endswitch;
                    $this->_options[$match[1]]['value'] = strlen($match[2]) > 0 ? $match[2] : null;
                endif;
                $this->arguments[$i] = null;
                unset($this->arguments[$i]);
            endif;
        endforeach;
    }

    public function showError($errorMessage) {
        $this->_logger($this->colors->getColoredString($errorMessage, "white", "red") . "\n", STDERR);
    }

    public function showMessage($errorMessage) {
        $this->_logger($this->colors->getColoredString($errorMessage, "white", "green") . "\n");
    }

    public function showNotice($errorMessage) {
        $this->_logger($this->colors->getColoredString($errorMessage, "blue", "yellow") . "\n");
    }

    public function run($argv) {
        if(sizeof($argv) < 1):
            $this->help();
            $this->showError('Error: Option not valid.');
            exit(1);
        endif;

        array_shift($argv);
        $this->arguments = $argv;
        $this->_parseOptions();
        $this->runTestScript();
        return true;
    }

    private function help() {
        $text = <<<DUMBO
▓█████▄  █    ██  ███▄ ▄███▓ ▄▄▄▄    ▒█████
▒██▀ ██▌ ██  ▓██▒▓██▒▀█▀ ██▒▓█████▄ ▒██▒  ██▒
░██   █▌▓██  ▒██░▓██    ▓██░▒██▒ ▄██▒██░  ██▒
░▓█▄   ▌▓▓█  ░██░▒██    ▒██ ▒██░█▀  ▒██   ██░
░▒████▓ ▒▒█████▓ ▒██▒   ░██▒░▓█  ▀█▓░ ████▓▒░
 ▒▒▓  ▒ ░▒▓▒ ▒ ▒ ░ ▒░   ░  ░░▒▓███▀▒░ ▒░▒░▒░
 ░ ▒  ▒ ░░▒░ ░ ░ ░  ░      ░▒░▒   ░   ░ ▒ ▒░
 ░ ░  ░  ░░░ ░ ░ ░      ░    ░    ░ ░ ░ ░ ▒
   ░       ░            ░    ░          ░ ░
 ░                                ░

DumboPHP 2.0 by Rantes
DumboTest shell executer.
Ussage:

    dumboTest [all|<unitTest>,<unitTest>,<unitTest>] [--option=value]

Options:
    --dir=dir/to/run
    --env=enviroment        Sets a particular enviroment for the execution
    --halt=[true|false]     Halt the script on error
    --watch                 Set a demon to watch files

DUMBO;
        fwrite(STDOUT, $text . "\n");
    }

    private function runTestScript() {
        $this->showNotice('Preparing tests...');
        $path = 'tests/';

        if (!empty($this->_options['dir']['value'])):
            $path = $this->_options['dir']['value'];
        endif;

        $watch = $this->_options['watch']['value'] ?? false;
        $path = realpath($path);
        substr($path, -1) !== '/' && ($path.='/');

        if(empty($this->arguments)) $this->arguments = ['all'];

        if (in_array('all', $this->arguments)):
            $testsDir = dir($path) or die('unknown path: '.$path);
            $this->arguments = [];
            while (($file = $testsDir->read()) !== FALSE):
                if($file != '.' and $file != '..' and preg_match('/(.+)\.php/', $file, $matches) === 1):
                    $this->arguments[] = substr($file, 0, -4);
                endif;
            endwhile;
                    //Second level, subdirectories
            $dir = opendir($path);
            while(false !== ($file = readdir($dir))):
                $npath = "{$path}{$file}";
                if ($file !== '.' and $file !== '..' and is_dir($npath) and is_readable($npath)):
                    $dir1 = opendir("{$path}{$file}");
                    if(false !== $dir1):
                        while(false !== ($file1 = readdir($dir1))):
                            if($file != '.' and $file != '..' and is_file("{$path}{$file}/{$file1}") and preg_match('/(.+)\.php/', $file1, $matches) === 1):
                                $this->arguments[] = substr("{$file}/{$file1}", 0, -4);
                            endif;
                        endwhile;
                        closedir($dir1);
                    endif;
                endif;
            endwhile;
            closedir($dir);
        endif;

        asort($this->arguments);

        if($watch):
            $files = [];
            $this->_logger('dumboTest_watcher', "Watching for changes in files: \n".implode("\n", $this->arguments));
            foreach($this->arguments as $file):
                $fullpath = "{$path}{$file}.php";
                $stats = stat($fullpath);
                $files[] = ['path'=> $fullpath, 'mtime' => $stats['mtime']];
            endforeach;
            $this->showNotice("\nWatching files...");
            $willRun = false;
            clearstatcache();
            while(true):
                foreach($files as  $index => $file):
                    $stats = stat($file['path']);
                    if($stats['size'] > 0 and $file['mtime'] !== $stats['mtime']):
                        $this->showNotice("File changed {$file['path']}");
                        $files[$index]['mtime'] = $stats['mtime'];
                        $willRun = true;
                        clearstatcache();
                    endif;
                endforeach;
                if ($willRun):
                    $tests = implode(' ', $this->arguments);
                    $command = "dumboTest {$tests} --dir={$path}";
                    $this->showNotice("\nRuning command: {$command}");
                    system($command);
                    $willRun = false;
                    $this->showNotice("\nWatching files...");
                endif;
            endwhile;
        else:
            $this->_runTests($path);
        endif;
    }
    /**
     * Set the methods and the lines which starts and ends
     */
    private function _getMethodLines($fileInput) {
        $file = new SplFileObject($fileInput);
        $methods = [];
        while (!$file->eof()):
            if (preg_match('@function\s([a-zA-Z0-9_]+)@', $file->current(), $matches) === 1):
                $methods[] = ['name' => $matches[1], 'line_start' => $file->key() + 1, 'line_end' => 0];
            endif;
            $file->next();
        endwhile;

        $lastLine = $file->key() - 1;
        $file->rewind();
        $lastkey = sizeof($methods) - 1;

        foreach($methods as $key => $method):
            $i = 0;
            $line = $key < $lastkey ? $methods[$key + 1]['line_start'] : $lastLine;

            do {
                $i++;
                if($line - $i <= 0) break;
                $file->seek($line - $i);
            } while(preg_match('@^[\s]*\}@', $file->current()) !== 1);

            $methods[$key]['line_end'] = $file->key() + 1;
        endforeach;

        return $methods;
    }

    /**
     * @TODO set proper coverage report to sonarcube
     */
    private function _runTests($path) {
        try {
            xdebug_set_filter(
                XDEBUG_FILTER_CODE_COVERAGE,
                XDEBUG_PATH_EXCLUDE,
                [
                    INST_PATH . 'migrations',
                    INST_PATH . 'app' . DIRECTORY_SEPARATOR . 'views',
                    INST_PATH . 'app' . DIRECTORY_SEPARATOR . 'webroot',
                    INST_PATH . 'tests',
                    get_include_path()
                ]
            );
            $testLaunch = new testDispatcher($this->arguments, $path, $this->_options['halt']['value']);
            $this->showNotice('Runing tests...');
            $start = microtime(true);
            $testCounter = 0;
            $tests = $this->arguments;
            $result = null;
            $testReport = new DOMDocument('1.0', 'UTF-8');
            $testReport->formatOutput = true;

            $testSuites = $testReport->createElement('testsuites');
            $testReport->appendChild($testSuites);

            $mainSuite = $testReport->createElement('testsuite');
            $mainSuite->setAttribute('name', 'Application Test Suite');
            $mainSuite->setAttribute('tests', 0);
            $mainSuite->setAttribute('assertions', 0);
            $mainSuite->setAttribute('failures', 0);
            $mainSuite->setAttribute('errors', 0);
            $mainSuite->setAttribute('warnings', 0);
            $mainSuite->setAttribute('skipped', 0);
            $mainSuite->setAttribute('time', 0);

            $testSuites->appendChild($mainSuite);
            $fails = 0;
            $totalCount = $notCovered = $lineRate = $packageLines = $fileLines = $packageTotalLines = 0;


            xdebug_start_code_coverage(XDEBUG_CC_UNUSED | XDEBUG_CC_DEAD_CODE);
            while (null !== ($test = array_shift($tests))):
                $suite = $testReport->createElement('testsuite');
                $suite->setAttribute('name', $test);
                $suite->setAttribute('errors', 0);
                $suite->setAttribute('warnings', 0);
                $suite->setAttribute('skipped', 0);

                $testLaunch->run($test);
                $testCounter++;

                $assertions = $times = $failures = 0;

                while (null !== ($case = array_shift($testLaunch->actions))):
                    //test
                    $assertions += $case['assertions'];
                    $times += $case['time'];
                    $failures += $case['fails'];
                    $testCase = $testReport->createElement('testcase');
                    $testCase->setAttribute('name', $case['test']);
                    $testCase->setAttribute('class', $test);
                    $testCase->setAttribute('classname', $test);
                    $testCase->setAttribute('file', $testLaunch->relativePathName);
                    $testCase->setAttribute('line', $case['line']);
                    $testCase->setAttribute('assertions', $case['assertions']);
                    $testCase->setAttribute('time', $case['time']);
                    $suite->appendChild($testCase);
                endwhile;

                $suite->setAttribute('assertions', $assertions);
                $suite->setAttribute('failures', $failures);
                $suite->setAttribute('time', $times);
                $suite->setAttribute('file', $testLaunch->relativePathName);
                $suite->setAttribute('tests', $testLaunch->tests);
                $mainSuite->appendChild($suite);
            endwhile;
            $coverageResult = xdebug_get_code_coverage();
            xdebug_stop_code_coverage();
            $includesPath = 'controllers|helpers|models';
            foreach($coverageResult as $entry => $lines):
                if(preg_match("@{$includesPath}@i", $entry) !== 1):
                    unset($coverageResult[$entry]);
                endif;
            endforeach;

            $took = microtime(true) - $start;
            $mainSuite->setAttribute('tests', $testCounter);
            $this->showNotice("\nFinished tests. Ran {$testLaunch->assertions} assertions in {$testCounter} tests. Finished in: {$took} seconds.");
            $mainSuite->setAttribute('time', $took);
            $mainSuite->setAttribute('assertions', $testLaunch->assertions);
            $mainSuite->setAttribute('failures', $fails);
            $testReport->save(INST_PATH.'test-result.xml');
            $this->showNotice("\nGenerating coverage report...");

            $generated = time();
            $coverageReport = new DOMDocument('1.0', 'UTF-8');
            $coverageReport->formatOutput = true;
            $coverageTag = $coverageReport->createElement('coverage');
            $coverageTag->setAttribute('generated', $generated);
            $coverageReport->appendChild($coverageTag);
            $projectTag = $coverageReport->createElement('project');
            $projectTag->setAttribute('timestamp', $generated);
            $coverageTag->appendChild($projectTag);
            $package = $coverageReport->createElement('package');
            $package->setAttribute('name', 'tests');

            $projectTag->appendChild($package);

            //coverage
            foreach($coverageResult as $entry => $lines):
                    $fileContent = new SplFileObject($entry);
                    $fileName = Camelize(basename($entry, '.php'));
                    $fileTag = $coverageReport->createElement('file');
                    $entryRelative = str_replace(INST_PATH, '', $entry);
                    $fileTag->setAttribute('name', $entryRelative);
                    $package->appendChild($fileTag);

                    $classTag = $coverageReport->createElement('class');
                    $classTag->setAttribute('name', $fileName);
                    $classTag->setAttribute('namespace', 'App');
                    $fileTag->appendChild($classTag);
                    $classInfo = $this->_getMethodLines($entry);

                    $fileLines = 0;
                    $linesCovered = [];
                    foreach($lines as $line => $hit):
                        switch ($hit):
                            case 1:
                                $linesCovered[] = $line;
                                $lineTag = $coverageReport->createElement('line');
                                $lineTag->setAttribute('num', $line);
                                $lineTag->setAttribute('type', 'stmt');
                                $lineTag->setAttribute('count', 3);
                                $fileTag->appendChild($lineTag);
                                $fileLines++;
                            break;
                            // case -2:
                            //     $fileContent->seek(($line - 1));
                            //     $lineText = trim($fileContent->current());
                            //     if(preg_match('@(\}|function|{)@i', $lineText) === 1):
                            //         $linesCovered[] = $line;
                            //         $lineTag = $coverageReport->createElement('line');
                            //         $lineTag->setAttribute('num', $line);
                            //         $lineTag->setAttribute('type', 'stmt');
                            //         $lineTag->setAttribute('count', 0);
                            //         $fileTag->appendChild($lineTag);
                            //         $fileLines++;
                            //     endif;
                            // break;
                        endswitch;
                    endforeach;

                    $coveredMethods = 0;
                    foreach($classInfo as $method):
                        foreach($linesCovered as $line):
                            if($method['line_start'] <= $line && $method['line_end'] >= $line):
                                $coveredMethods++;
                                break;
                            endif;
                        endforeach;
                    endforeach;

                    $classMetricsTag = $coverageReport->createElement('metrics');
                    $classMetricsTag->setAttribute('complexity', 24);
                    $classMetricsTag->setAttribute('methods', sizeof($classInfo));
                    $classMetricsTag->setAttribute('coveredmethods', $coveredMethods);
                    $classMetricsTag->setAttribute('statements', $fileLines);
                    $classTag->appendChild($classMetricsTag);
            endforeach;

            file_put_contents(INST_PATH.'coverage.xml', $coverageReport->saveXML());
            $this->showNotice('Coverage report generated at coverage.xml');
        } catch (Throwable $e) {
            $this->_failed = true;
            $this->showError("\n{$e}");
            exit(1);
        }
    }

}
$shell = new dumboShell();
$shell->run($argv);
?>
