#!/usr/bin/env php
<?php

#region required scripts
require $_composer_autoload_path ?? __DIR__ . '/../vendor/autoload.php';
require __DIR__ . '/doc';
require __DIR__ . '/postman';
require __DIR__ . '/version';
require __DIR__ . '/util';
#endregion required scripts

// #region Define the total supported options argument list
if (!defined('HTr_MAX_ARGV')) {
    define('HTr_MAX_ARGV', 1000);
}

if (!defined('HTR_SUPPORTED_EXTENSION')) {
    define('HTR_SUPPORTED_EXTENSION', ['json', 'yaml']);
}
// #endregion Define the total supported options argument list

use Drewlabs\Htr\Utilities\Console;
use Drewlabs\Htr\Project;
use Drewlabs\Htr\ProjectTests;
use Drewlabs\Htr\RandomID;

/**
 * HTr usage help message
 * 
 * @param string $version 
 * @return string 
 */
function htr_help_function()
{
    $message = <<<EOT
    A fast and simple HTTP request Test runner
    
    USAGE:
        ./vendor/bin/htr <SUBCOMMAND> [OPTIONS]
    
    FLAGS:
        -h, --help
            Prints help information

    SUBCOMMAND
        test                Exectue an HTTP Test
        doc                 Creates a markdown documentation
        postman             Generate postman collection for htr configuration document
    
    OPTIONS:
        --input             Path to the json / yaml input
        --verbose           Execute script in debug mode printing every execution stack to the console
        --output            Path to test logs
        --req               List of requests to execute in the configuration file
        --directory         List of request directories to execute
        --json              Tells HTr to parse json formatted input file. Default: YAML
        --env               Pass environemnt variable to the script. Environment variable passed to the script will override variables from input file. (Ex: --env=_var:value)


EOT;
    return $message;
}

// #region IO
/**
 * Recursively create directory if the later does not exists
 * 
 * @param string $dirname 
 * @return void 
 */
function htr_create_directory_if_not_exists(string $dirname)
{
    // Create the path directory if not exists
    if (!is_dir($dirname)) {
        mkdir($dirname, 0777, true);
    }
}

function htr_resolve_parent_directory_path(string $path, string $base = __DIR__)
{
    $substr = substr($path, 0, 3);
    // Handle relative path
    if (('../' === $substr) || ('..\\' === $substr)) {
        $directory = $base;
        $_substr = substr($path, 0, 3);
        while (('../' === $_substr) || ('..\\' === $_substr)) {
            $directory = dirname($directory);
            $path = substr($path, 3);
            $_substr = substr($path, 0, 3);
        }
        $path = $directory . DIRECTORY_SEPARATOR . $path;
    }

    return $path;
}

function htr_resolve_relative_path($path, string $base = __DIR__)
{
    $substr = substr($path, 0, 2);
    // Handle relative path
    if (('./' === $substr) || ('.\\' === $substr)) {
        $path = $base . DIRECTORY_SEPARATOR . substr($path, 2);
    }
    return $path;
}

function htr_resolve_path(string $path, string $base = __DIR__)
{
    $substr = substr($path, 0, 3);
    $path = ('../' === $substr) || ('..\\' === $substr) ? htr_resolve_parent_directory_path($path, $base) : (($subsustr = substr($substr, 0, 2)) && (('./' === $subsustr) || ('.\\' === $subsustr)) ? htr_resolve_relative_path($path, $base) : $path);
    // If the path does not starts with '/' we append the current 
    if ('/' !== substr($path, 0, 1)) {
        $path = $base . DIRECTORY_SEPARATOR . $path;
    }
    return $path;
}
// #endregion IO

// #region Command line utilities
/**
 * Resolve command options from command arguments
 * 
 * @param array $args 
 * @return array<array-key, mixed> 
 */
function htr_cmd_options(array $args)
{
    $index = 0;
    $configs = array();
    while ($index < HTr_MAX_ARGV && isset($args[$index])) {
        if (preg_match('/^([^-\=]+.*)$/', $args[$index], $matches) === 1) {
            // not have ant -= prefix
            $configs[$matches[1]] = true;
        } else if (preg_match('/^-+(.+)$/', $args[$index], $matches) === 1) {
            // match prefix - with next parameter
            if (preg_match('/^-+(.+)\=(.+)$/', $args[$index], $subMatches) === 1) {
                if (array_key_exists($subMatches[1], $configs)) {
                    $configs[$subMatches[1]] = array_merge(is_array($configs[$subMatches[1]]) ? $configs[$subMatches[1]] : [$configs[$subMatches[1]]], [$subMatches[2]]);
                } else {
                    $configs[$subMatches[1]] = $subMatches[2];
                }
            } else if (isset($args[$index + 1]) && preg_match('/^[^-\=]+$/', $args[$index + 1]) === 1) {
                // have sub parameter
                if (array_key_exists($matches[1], $configs)) {
                    $configs[$matches[1]] = array_merge(is_array($configs[$matches[1]]) ? $configs[$matches[1]] : [$configs[$matches[1]]], [$args[$index + 1]]);
                } else {
                    $configs[$matches[1]] = $args[$index + 1];
                }
                $index++;
            } elseif (strpos($matches[0], '--') === false) {
                for ($j = 0; $j < strlen($matches[1]); $j += 1) {
                    $configs[$matches[1][$j]] = true;
                }
            } else if (isset($args[$index + 1]) && preg_match('/^[^-].+$/', $args[$index + 1]) === 1) {
                if (array_key_exists($matches[1], $configs)) {
                    $configs[$matches[1]] = array_merge(is_array($configs[$matches[1]]) ? $configs[$matches[1]] : [$configs[$matches[1]]], [$args[$index + 1]]);
                } else {
                    $configs[$matches[1]] = $args[$index + 1];
                }
                $index++;
            } else {
                $configs[$matches[1]] = true;
            }
        }
        $index++;
    }

    return $configs;
}

/**
 * Resolve command arguments from command parameters
 * 
 * @param array $args 
 * @return array 
 */
function htr_cmd_arguments(array $args)
{
    return array_values(array_filter($args, function ($arg) {
        return substr($arg, 0, 1) !== '-' && substr($arg, 0, 2) !== '--';
    }));
}

/**
 * Prepare environment passed through command line
 * 
 * @param array $values 
 * @return array 
 */
function htr_prepare_env(array $values)
{
    $output = [];

    foreach ($values as $value) {
        $key = trim(explode(':', $value)[0] ?? '');
        $v = empty($key) ? $value : substr($value, strlen($key) + 1);
        // To make sure no empty string is passed as array key we set the key to 0 if the key is empty
        $k = empty($key) ? 0 : $key;
        $output[$k] = $v;
    }
    return $output;
}
// #endregion Command line utilities

//#region Tests runner
/**
 * 
 * @param string $format 
 * @return array 
 */
function htr_get_extensions(string $format = 'yaml')
{
    switch (strtolower($format)) {
        case 'yml':
        case 'yaml':
            return ['yaml', 'yml'];
        case 'json':
            return ['json'];

        default:
            return [];
    }
}
/**
 * executes tests defines in the input path
 * 
 * @param string $inputPath 
 * @param array $options 
 * @param string $output 
 * @return void 
 */
function htr_run_tests(string $inputPath, array $options, string $outputPath = null)
{

    // Resolve the realpath to the path specified by the program user
    $path = @htr_resolve_path($inputPath, __DIR__);

    $attributes = isset($options['json']) && boolval($options['json'])  ? @json_decode(file_get_contents($path), true) : \yaml_parse(file_get_contents($path));

    if (!is_array($attributes) || empty($attributes)) {
        echo Console::white(sprintf("Failed to parse configuration file located at %s", $inputPath), null, 'red') . PHP_EOL . PHP_EOL;
        return;
    }

    // We merge any environment variable passed in as parameter with values from console
    if (isset($options['env'])) {
        $attributes['env'] =  array_merge($attributes['env'] ?? [], htr_prepare_env(is_array($envParam = $options['env'] ?? []) ? $envParam : [$envParam]));
    }

    // #region Create the output function
    $then = $outputPath ? function ($output, array $failures = []) use ($outputPath) {
        if (is_string($output)) {
            if (is_dir($outputPath)) {
                $outputPath = $outputPath . DIRECTORY_SEPARATOR . sprintf("%s.log", RandomID::new()->__invoke());
            }
            $directoryExists = is_dir(dirname(strval($outputPath)));
            $actualOuputPath =  $directoryExists ? $outputPath : @htr_resolve_path(RandomID::new()->__invoke());

            // Case the directory the directory does not exist we tell user where out will be written to
            if (!$directoryExists) {
                // Log to the console, with file not exists error
                echo Console::yellow("\nCannot locate output path at " . (strval($outputPath)) . " Writing output to $actualOuputPath") . PHP_EOL;
            }

            // Write tests log file
            file_put_contents($actualOuputPath, $output);

            // Complete the execution
            echo (count($failures) !== 0 ? Console::normal(sprintf("\n\nExecution completed with some test errors. Check %s for more info", $actualOuputPath), null, 'red') : Console::normal(sprintf("\n\nExecution completed. Check %s for more info", $actualOuputPath), null, 'green')) . PHP_EOL;
            echo PHP_EOL;
        }
    } : function () {
        echo Console::normal(sprintf("\n\nExecution completed. Thanks for using the program"), null, 'green') . PHP_EOL;
        echo PHP_EOL;
    };
    
    // #endregion Create the output function
    // We invoke Requests Test execution object to execute the requests
    $client = isset($options['verbose']) && boolval($options['verbose']) ? ProjectTests::new(Project::fromAttributes($attributes))->debug() : ProjectTests::new(Project::fromAttributes($attributes));
    $requests = array_merge(isset($options['request']) ? (is_array($options['request']) ? $options['request'] : [$options['request']]) : [], isset($options['req']) ? (is_array($options['req']) ? $options['req'] : [$options['req']]) : []);
    $directories = array_merge(isset($options['directory']) ? (is_array($options['directory']) ? $options['directory'] : [$options['directory']]) : [], isset($options['d']) ? (is_array($options['d']) ? $options['d'] : [$options['d']]) : []);

    echo Console::normal("Running tests at " . $inputPath, true) . PHP_EOL;
    echo PHP_EOL;
    // Execute the propject requests
    $client->setDirectories($directories)->setRequets($requests)->execute($then);
}

/**
 * 
 * @param string $directory 
 * @param string $format 
 * @param array $options 
 * @return void 
 * @throws Exception 
 */
function htr_run_test_in_directory(string $directory, string $format, array $options)
{
    $paths = array_filter(array_diff(scandir($directory)  ?? [], array('..', '.')), function ($path) use ($format) {
        return null !== $path && !empty(array_intersect([substr($path, strrpos($path, '.') + 1)], htr_get_extensions($format)));
    });

    // foreach path, we run test runner and output log to the path+log
    foreach ($paths as $path) {
        $inputPath = sprintf("%s%s%s", $directory, DIRECTORY_SEPARATOR, $path);
        htr_run_tests($inputPath, $options ?? [], sprintf("%s%s%s", $directory, DIRECTORY_SEPARATOR, sprintf("%slog", str_replace(substr($path, strrpos($path, '.') + 1), '', $path))));
    }
}

/**
 * execute tests using HTr
 *  
 * @param string $path 
 * @param mixed $format 
 * @param array $options 
 * @return void 
 * @throws Exception 
 */
function htr_run_tests_program(string $path, $format, array $options)
{
    if (is_dir($path)) {
        return htr_run_test_in_directory($path, $format, $options);
    }
    return htr_run_tests($path, $options, $options['output'] ??  null);
}
//#endregion Tests runner

/**
 * Main program
 * 
 * @param array $args 
 * @return void 
 * @throws Error 
 * @throws RuntimeException 
 */
function main(array $args = [])
{
    echo Console::normal("HTr - HTTP Request Test Runner", null, 'blue') . PHP_EOL . PHP_EOL;

    // #region Load command line arguments and options
    // In case the list of arguments starts with - or --, the command input configuration is the last parameter, else it's the first parameter
    if (empty($args)) {
        list($optionsArgs, $command) = [[], null];
    } else if ('-' === substr(strval($args[0]), 0, 1)) {
        // Case the total list of argument is 1 or the last element starts with - or --, we do not treat the last argument as command argument
        $optionsArgs = array_slice($args, 0, ((count($args) === 1) || ('-' === substr(strval($args[count($args) - 1]), 0, 1)) ? null : count($args) - 1));
        $command = array_slice($args, count($optionsArgs))[0] ?? null;
    } else {
        $command = $args[0];
        $optionsArgs = array_slice($args, 1);
    }
    $options = htr_cmd_options($optionsArgs);
    // #endregion Load command line arguments and options

    if (isset($options['help'])) {
        echo htr_help_function(HTr_VERSION);
        return;
    }

    $path = $options['input'] ?? null;
    if (null === $path) {
        echo htr_help_function(HTr_VERSION);
        return;
    }

    // Set the memory limit for the current script execution
    ini_set('memory_limit', '-1');
    set_time_limit(0);

    // Resolve the realpath to the path specified by the program user
    $path = @htr_resolve_path($path, __DIR__);
    $options['format'] = isset($options['json']) ? 'json' : 'yaml';
    $format = $options['format'] ?? 'yaml';

    if (!in_array($format, HTR_SUPPORTED_EXTENSION)) {
        echo Console::white("Supported input formats are json,yaml", null, 'red') . PHP_EOL . PHP_EOL;
        return;
    }

    switch (strtolower($command)) {
        case 'test':
            # code...
            htr_run_tests_program($path, $format, $options);
            break;
        case 'doc':
        case 'docs':
            # code...
            htr_doc_build($path, $options, $options['output'] ??  sprintf("%s%s%s.md", __DIR__,DIRECTORY_SEPARATOR, RandomID::new()->__invoke()), __DIR__);
            break;
        case 'postman':
            # code...
            htr_postman_build($path, $options, $options['output'] ??  sprintf("%s%s%s.md", __DIR__,DIRECTORY_SEPARATOR, RandomID::new()->__invoke()), __DIR__);
            break;
        default:
            # code...
            htr_help_function();
            break;
    }
}

main(array_slice($argv ?? [], 1));
