#!/usr/bin/env bash
# -*- tab-width: 4; encoding: utf-8 -*-
#
#
#

PS4='+ ${FUNCNAME:-main}${LINENO:+:$LINENO}>'

# If it works with failglob and nounset, it will work without.
shopt -s failglob
set -o nounset

. ./argsparse.sh

shopt -s extdebug
errors=0
disabled=0

rm -f unittest.log unittest.env.log

err_trap() {
	: $((errors+=1))
	return 1
}

trap err_trap ERR

print_exit_status() {
    # Prints [OK] at the end of the screen of first argument is 0,
    # else [FAILURE].
    # 1st Parameter: a number, usually the exit status of your
    # previous command. If omitted, will use $?.
    # returns the first parameter value
    local ret="${1:-$?}"
    # If you want you can override the FAILED and OK messages by
    # pre-defining those variables.
    local FAILED=${FAILED:-FAILED}
    local OK=${OK:-OK}
    # Move to column default is 70
    local COL=${COL:=70}
    [[ -t 1 ]] && echo -en "\033[${COL}G"
    # Colors
    if [[ -t 1 ]]
    then
        local COLOR_SUCCESS=${COLOR_SUCCESS:-'\033[1;32m'}
        local COLOR_FAILURE=${COLOR_FAILURE:-'\033[1;31m'}
        local COLOR_WARNING='\033[1;33m' COLOR_NORMAL='\033[0;39m'
    else
        local COLOR_SUCCESS= COLOR_FAILURE= COLOR_WARNING= COLOR_NORMAL=
    fi
    [[ "$ret" -eq 0 ]] && echo -e "[$COLOR_SUCCESS$OK$COLOR_NORMAL]" || \
		echo -e "[$COLOR_FAILURE$FAILED$COLOR_NORMAL]"
    return $ret
}

echo_for_print_status() {
    local m=$1
    if tty >/dev/null 2>&1 && [[ -n "${PRINT_STATUS_COLOR:-}" ]]
    then
        m="$(tput setaf $PRINT_STATUS_COLOR)$m$(tput sgr0)"
    fi
    printf "%s: " "$m"
}

exec_and_print_status() {
    # prints a message, execute a command and print its exit status
    # using print_exit_status function.
    # 1st Parameter: a message
    # all other parameter: the command to execute
    # returns the exit status code of the command
    [[ $# -lt 2 ]] && return 1
    local m=$1 ; shift
    PRINT_STATUS_COLOR="${PRINT_STATUS_COLOR-}" \
		echo_for_print_status "$m"
    "$@"
    print_exit_status $?
}

default_test() {
	[[ $? -eq 0 ]]
}

failure() {
	[[ $? -ne 0 ]]
}

shell_env() {
	# Greps are:
	# * variables automatically altered by bash itself.
	# * local environment variables, used by the environment checking routines.
	# * argsparse private variables, subjects to modifications/deletion.
	# * argsparse public variables.
	# * argsparse results, which are expected to change.
	set | \
		grep -vE '^(FIRST|SECOND|SHELLOPTS|FUNCNAME|_|LINENO|BASH_(ARG[CV]|LINENO|SOURCE|REMATCH)|PIPESTATUS)=' | \
		grep -vE '^before=' | \
		grep -vE '^__argsparse_(options_descriptions|short_options|tmp_identifiers)=' | \
		grep -vE '^argsparse_usage_description=' | \
		grep -vE '^(program_(params|options))|cumulated_values_[0-9a-zA-Z_]+='
	shopt
	set -o | grep -v xtrace
}

check_env() {
	{
		printf "For test: %s\n" "$message"
		diff -u <(echo "$before") <(shell_env)
	} >> unittest.env.log
}

parse_option_wrapper() {
	local message=$1
	local TEST=${TEST:-default_test}
	shift
	local before=$(shell_env)
	echo_for_print_status "Checking $message"
	(
		printf "Test is: %s\n" "$message"
		printf "Validation is: %s\n" "$TEST"
		set -x
		(
			trap check_env EXIT
			argsparse_parse_options "$@"
			${INNERTEST:-exit $?}
		)
	) >>unittest.log 2>&1
	$TEST
	print_exit_status || exit
}

(
	argsparse_usage_description="Usage has been triggered"
	echo_for_print_status "Checking no parameter triggers usage"
	( 
		argsparse_parse_options
	) | grep -q "$argsparse_usage_description"
	[ ${PIPESTATUS[0]} -ne 0 -a ${PIPESTATUS[1]} -eq 0 ]
	print_exit_status 
)

(
	argsparse_allow_no_argument yes
	argsparse_use_option option ""
	parse_option_wrapper "argsparse_allow_no_argument yes"
)

(
	argsparse_usage_description="Usage has been triggered"
	echo_for_print_status "Checking dummy option triggers usage"
	( 
		argsparse_parse_options --dumb 2>/dev/null
	) | grep -q "$argsparse_usage_description"
	[ ${PIPESTATUS[0]} -ne 0 -a ${PIPESTATUS[1]} -eq 0 ]
	print_exit_status 
)

(
	argsparse_use_option first-option "first option description"
	echo_for_print_status "Checking if option description appear in usage"
	(
		argsparse_parse_options -h
	) | grep -q "first option description"
	print_exit_status
)

(
	argsparse_use_option shortcut ""
	parse_option_wrapper "option detection" --shortcut

	argsparse_set_option_property short:S shortcut
	parse_option_wrapper "short property" -S
)

(
	argsparse_use_option =shortcut ""
	parse_option_wrapper "= in optstring" -s
)

(
	argsparse_use_option option1 "" type:weird:value
	echo_for_print_status "Checking 'weird:value' property value"
	[[ $(argsparse_has_option_property option1 type) = 'weird:value' ]]
	print_exit_status
)

(
	argsparse_use_option option1 ""
	for weirdo in '?' , '*' '!'
	do
		(
			echo_for_print_status "Checking forbidden '$weirdo' property value"
			argsparse_set_option_property type:"foo${weirdo}bar" option1 2>/dev/null
			failure
			print_exit_status
		)
	done
)

value_check_test() {
	[[ ${program_options[$value_check_option]} = $value_check_value ]]
}

value_check() {
	local value_check_option=$1
	local value_check_value=$2
	shift 2
	INNERTEST=value_check_test parse_option_wrapper "$@"
}

(
	argsparse_use_option value ""
	argsparse_set_option_property value value
	TEST=failure parse_option_wrapper "if value property expects value" --value
	value_check value 1 "if value is correctly detected" --value 1
)

(
	argsparse_use_option value: ""
	value_check value 1 "if value is correctly detected with optstring" --value 1
	TEST=failure parse_option_wrapper "if value property expects value if set with optstring" --value
)	

(
	argsparse_use_option option1 ""
	argsparse_use_option option2 "" mandatory
	TEST=failure parse_option_wrapper "if missing mandatory option triggers error" --option1
	parse_option_wrapper "if mandatory option makes return code 0" --option1 --option2
	argsparse_use_option option3 "" mandatory
	TEST=failure parse_option_wrapper "if missing mandatory options trigger error (1)" --option1
	TEST=failure parse_option_wrapper "if missing mandatory options trigger error (2)" --option1 --option2
	TEST=failure parse_option_wrapper "if missing mandatory options trigger error (3)" --option1 --option3
	parse_option_wrapper "if missing mandatory options trigger error (4)" --option3 --option2
)

cumul_test() {
	[[ $? -ne 0 ]] && return 1
	local cumul="cumulated_values_$option[@]"
	# Shoud never trigger nounset
	local -a array=( "${!cumul}" )
	local i
	[[ ${#array[@]} -eq "${#cumul_array[@]}" ]] || return 1
	for ((i=0 ; i < ${#array[@]} ; i++))
	do
		[[ ${array[$i]} = "${cumul_array[$i]}" ]] || return 1
	done
	return 0
}

(
	argsparse_use_option option1 "" cumulative
	option=option1
	cumul_array=(1 2 1 2)
	INNERTEST=cumul_test parse_option_wrapper "cumulative property behaviour" "${cumul_array[@]/#/--option1=}"
)

(
	option=option2
	cumul_array=(1 2)
	argsparse_use_option option2 "" cumulativeset
	INNERTEST=cumul_test parse_option_wrapper "cumulativeset property behaviour" "${cumul_array[@]/#/--option2=}" "${cumul_array[@]/#/--option2=}"
)

(
	argsparse_use_option option1 ""
	argsparse_use_option option2 "" alias:option1
	value_check option1 1 "simple alias" --option1
	argsparse_use_option option3 "" alias:option2
	value_check option1 1 "recursive alias" --option1
)

(
	argsparse_use_option option1 ""
	argsparse_use_option option2 "" require:option1
	parse_option_wrapper "simple require (1)" --option2 --option1
	TEST=failure parse_option_wrapper "simple require (2)" --option2
	argsparse_use_option option3 "" require:option2
	TEST=failure parse_option_wrapper "recursive require (1)" --option3
	TEST=failure parse_option_wrapper "recursive require (2)" --option3 --option2
	parse_option_wrapper "recursive require (3)" --option3 --option2 --option1
)

(
	argsparse_use_option option1 ""
	argsparse_use_option option2 "" exclude:option1
	TEST=failure parse_option_wrapper "exclusion" --option2 --option1
)

(
	argsparse_use_option option1 ""
	argsparse_use_option option2 "" default:1 exclude:option1
	parse_option_wrapper "exclusion does not prevent default values" --option1
)

(
	argsparse_use_option option1 ""
	argsparse_use_option option2 ""
	argsparse_use_option option3 "" require:"option1 option2"
	TEST=failure parse_option_wrapper "multiple require (1)" --option3
	TEST=failure parse_option_wrapper "multiple require (2)" --option3 --option2
	TEST=failure parse_option_wrapper "multiple require (3)" --option3 --option1
	parse_option_wrapper "multiple require (4)" --option3 --option2 --option1
)


(
	declare -a option_option1_values=(accept)
	argsparse_use_option option1: ""
	TEST=failure parse_option_wrapper "accepted values (bad value)" \
		--option1 foo
	parse_option_wrapper "accepted values (good value)" --option1 accept
)

dir=$(mktemp -d)
file=$(mktemp --tmpdir="$dir")
declare -A types=(
	[link]="$dir/link1 $dir/link2"
	[file]="$file $dir/link1"
	[directory]=". .. $dir $dir/linkdir"
	[char]="c . 0 @"
	[unsignedint]=123
	[int]="1 123 0 -1 -938945349"
	[hexa]="0x123abc 71234 abc"
	[ipv4]="192.168.40.254 127.0.0.1 1.2.3.4"
	[ipv6]="2001:7a8:b018::1 ::1 2001:7a8:b018:0:21f:c6ff:fe59:71fd"
	[username]=$(whoami)
	[group]=$(id -gn)
	[port]="ssh 80"
	[portnumber]=1234
)

fifo="$dir/fifo"
if mkfifo "$fifo"
then
	types[pipe]=$fifo
else
	printf "Could not create persistent fifo. Disabling pipe type test.\n"
	: $((disabled++))
fi

for socket in /dev/log
do
	[[ -S $socket ]] || continue
	types[socket]=$socket
done

# Look for a terminal
for stdfd in 0
do
	[[ -t $stdfd ]] || continue
	types[terminal]=$stdfd
done

for t in terminal socket
do
	if [[ ${types[$t]-unset} = unset ]]
	then
		: $((disabled++))
		printf "No %s found: won't test.\n" "$t"
	fi
done

# Default 'host' type test case:
types[host]="${types[ipv4]} ${types[ipv6]}"
# Test hostnames only if we can actually resolv.
if host localhost >/dev/null 2>&1
then
	types[hostname]="livna.org localhost"
	types[host]+=" ${types[hostname]}"
else
	printf "Cannot resolv localhost. Not testing hostname type.\n"
	: $((disabled++))
fi

(cd "$dir" && ln -s "$file" link1 && ln -s asdf link2 && ln -s . linkdir)

for type in "${!types[@]}"
do
	(
		argsparse_use_option option: "" "type:$type"
		# Left unquoted on purpose. There's no funny chars in this array.
		i=1
		for value in ${types[$type]}
		do
			parse_option_wrapper "type '$type' ($i)" --option "$value"
			: $((i++))
		done
	)
done

declare -A bad_types=(
	[file]=". .. $dir $fifo"
	[directory]="${types[file]} $fifo $dir/link1"
	[pipe]="${types[file]} ${types[dir]-}"
	[socket]="${types[file]} $fifo $dir/link1"
	[link]="$file $fifo"
	[char]="12 abc"
	[unsignedint]="-1 -2234958 abc"
	[int]="a b casdf"
	[ipv4]="${types[ipv6]}"
	[ipv6]="${types[ipv4]}"
	[host]="livna.org1"
	[username]="asdkfjasdkfanfk1234"
	[group]="asdkfjasdkfanfk1234"
	[port]="12345678 foobar"
	[portnumber]=12345678
	[hostname]="livna.org1 google.com1"
)

for type in "${!bad_types[@]}"
do
	(
		argsparse_use_option option: "" "type:$type"
		# Left unquoted on purpose. (again)
		i=1
		for value in ${bad_types[$type]}
		do
			TEST=failure parse_option_wrapper "failure with type '$type' ($i)" --option "$value"
			: $((i++))
		done
	)
done

(
	argsparse_use_option option: "" "type:terminal"
	TEST=failure parse_option_wrapper "failure with type 'terminal' (1)" --option 0 </dev/null
)
rm -Rf "$dir"

(
	check_option_type_unittest() {
		local value=$1
		[[ $value = 1 ]]
	}
	argsparse_use_option option: "" type:unittest
	parse_option_wrapper "custom type (1)" --option 1
	TEST=failure parse_option_wrapper "custom type (2)" --option asdf
)

printf "Tests report:\n"
if [[ $disabled -ne 0 ]]
then
	printf "* %d test(s) disabled.\n" "$disabled"
fi

if [[ $errors -ne 0 ]]
then
	printf "* %d error(s) encountered. (See above)\n" "$errors"
	exit_code=1
else
	printf "All tests passed.\n"
	exit_code=0
fi

printf "Environment alteration detected:\n"
if grep -v -B 1 '^For test:' unittest.env.log
then
	: exit_code $((exit_code))
else
	printf "None.\n"
fi

if [[ $exit_code -ne 0 ]]
then
	printf "Runtime environment was:\n"
	set -x
	{
		command -v bash
		bash --version
		shopt
		set -o
	} 2>&1
fi

printf "Completed in %d seconds.\n" "$SECONDS"

exit "$exit_code"
