#############################################################################
##
##   Unit testing automation script
##   Last updated on July 22, 2026
##
##   This file is part of Logtalk <https://logtalk.org/>
##   SPDX-FileCopyrightText: 1998-2026 Paulo Moura <pmoura@logtalk.org>
##   SPDX-License-Identifier: Apache-2.0
##
##   Licensed under the Apache License, Version 2.0 (the "License");
##   you may not use this file except in compliance with the License.
##   You may obtain a copy of the License at
##
##       http://www.apache.org/licenses/LICENSE-2.0
##
##   Unless required by applicable law or agreed to in writing, software
##   distributed under the License is distributed on an "AS IS" BASIS,
##   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
##   See the License for the speci}c language governing permissions and
##   limitations under the License.
##
#############################################################################


#Requires -Version 7.3

[CmdletBinding()]
param(
	[Parameter()]
	[String]$p,
	[String]$o = "verbose",
	[String]$m = "normal",
	[String]$f = "default",
	[String]$d,
	# disable timeouts to maintain backward compatibility
	[String]$t = 0,
	[String]$j = 1,
	[String]$n = "tester",
	[String]$s,
	[String]$b,
	[String]$u,
	[String]$c = "none",
	[String]$l,
	[String]$e,
	[String]$i,
	[String]$g = "true",
	[String]$r,
	[String]$testset,
	[Switch]$w,
	[Switch]$z,
	[String[]]$a,
	[Switch]$v,
	[Switch]$h
)

Function Write-Script-Version {
	$myFullName = $MyInvocation.ScriptName
	$myName = Split-Path -Path "$myFullName" -leaf -Resolve
	Write-Output "$myName 23.0"
}

Function Format-Decimal {
    param(
        [double]$Number,
        [int]$DecimalPlaces
    )
    return [Math]::Round($Number, $DecimalPlaces)
}

Function Invoke-CompletionNotification {
	param(
		[Parameter(Mandatory = $true)]
		[int]$ExitCode
	)

	if (-not $z -or [Console]::IsOutputRedirected) {
		return
	}

	$use_native_beep = $true

	try {
		if ($ExitCode -eq 0) {
			[Console]::Beep(1760, 120)
			Start-Sleep -Milliseconds 100
			[Console]::Beep(2093, 160)
		} else {
			[Console]::Beep(880, 140)
			Start-Sleep -Milliseconds 50
			[Console]::Beep(880, 140)
			Start-Sleep -Milliseconds 50
			[Console]::Beep(698, 260)
			Start-Sleep -Milliseconds 50
			[Console]::Beep(698, 260)
			Start-Sleep -Milliseconds 50
			[Console]::Beep(698, 260)
		}
	} catch {
		$use_native_beep = $false
	}

	if (-not $use_native_beep) {
		if ($ExitCode -eq 0) {
			[Console]::Write("`a")
			Start-Sleep -Milliseconds 100
			[Console]::Write("`a")
		} else {
			[Console]::Write("`a")
			Start-Sleep -Milliseconds 50
			[Console]::Write("`a")
			Start-Sleep -Milliseconds 50
			[Console]::Write("`a")
			Start-Sleep -Milliseconds 50
			[Console]::Write("`a")
			Start-Sleep -Milliseconds 50
			[Console]::Write("`a")
		}
	}
}

Function ConvertTo-ArgumentString {
	param(
		[Parameter(Mandatory = $true)]
		[Object[]]$Arguments
	)

	# Build a fully quoted command line to preserve arguments containing spaces
	# and quotes when spawning worker processes on Windows.
	$quoted = foreach ($argument in $Arguments) {
		$arg = [string]$argument

		if ($arg.Length -eq 0) {
			'""'
			continue
		}

		if ($arg -notmatch '[\s"]') {
			$arg
			continue
		}

		$escaped = $arg -replace '(\\*)"', '$1$1\\"'
		$escaped = $escaped -replace '(\\+)$', '$1$1'
		'"' + $escaped + '"'
	}

	return ($quoted -join ' ')
}

Function Invoke-TestSet() {
param(
	[Parameter(Position = 0)]
	[String]$path
)
	$unit = (Split-Path -Path $path) -replace '\\', '/'
	$unit_short = $unit -replace $prefix, ""
	Push-Location "$unit"
	if ($w -eq $true) {
		if (Test-Path .\.lgt_tmp) {
			Remove-Item -Path .\.lgt_tmp -Recurse -Force
		}
		if (Test-Path .\lgt_tmp) {
			Remove-Item -Path .\lgt_tmp  -Recurse -Force
		}
	}
	if ($o -eq "verbose") {
		if (Test-Path -Path "$unit/VERSION.packs") {
			$version_packs = Get-Content -Path "$unit/VERSION.packs"
			Write-Output "% $unit_short (pack version $version_packs)"
		} else {
			Write-Output "% $unit_short"
		}
	}
	$source = ".\\$n.ps1"
	if (Test-Path $source) {
		& $source @allargs
		if ($LASTEXITCODE -gt 0) {
			Write-Output "%         source $n.ps1 returned code $LASTEXITCODE"
			Exit 9
		}
	}
	# convert any forward slashes so that the derived file name is usable
	# also convert any colon if running on Windows systems so that the derived file name is usable
	$name = (($unit -replace '/', '__') -replace '\\', '__') -replace ':', '___'
	$report_goal = "logtalk_load(lgtunit(automation_report)),set_logtalk_flag(test_results_directory,'$results'),set_logtalk_flag(test_unit_name,'$name')"
	if ($s -ne "") {
		$flag_goal = "set_logtalk_flag(suppress_path_prefix,'$prefix')"
	} else {
		$flag_goal = "true"
	}
	if ($issue_server -ne "") {
		$flag_goal = "logtalk_load(issue_creator(loader)),set_logtalk_flag(issue_server,'$issue_server'),set_logtalk_flag(issue_labels,'$issue_labels'),$flag_goal"
	}
	if ($u -ne "") {
		$flag_goal = "set_logtalk_flag(tests_base_url,'$u'),$flag_goal"
	}
	if ($f -ne "default" -and $c -ne "none") {
		$flag_goal = "set_logtalk_flag(tests_report_directory,'$unit/'),$flag_goal"
	}
	if ($m -eq "optimal" -or $m -eq "all") {
		$tests_exit = Invoke-Tests $name "$initialization_goal,$report_goal,$format_goal,$coverage_goal,$flag_goal,$seed_goal,$tester_optimal_goal"
		$mode_prefix="% (opt)   "
	} elseif ($m -eq "normal" -or $m -eq "all") {
		$tests_exit = Invoke-Tests $name "$initialization_goal,$report_goal,$format_goal,$coverage_goal,$flag_goal,$seed_goal,$tester_normal_goal"
		$mode_prefix="%         "
	} elseif ($m -eq "debug" -or $m -eq "all") {
		$tests_exit = Invoke-Tests $name "$initialization_goal,$report_goal,$format_goal,$coverage_goal,$flag_goal,$seed_goal,$tester_debug_goal"
		$mode_prefix="% (debug) "
	}

	if ($tests_exit -eq 0 -and $o -eq "verbose" -and (Select-String -Path "$results/$name.results" -Pattern "tests skipped" -SimpleMatch -Quiet)) {
		Write-Output "%         skipped"
	} elseif ($tests_exit -eq 0 -and $o -eq "verbose" -and (Select-String -Path "$results/$name.results" -Pattern "(not applicable)" -SimpleMatch -Quiet)) {
		Write-Output "%         not applicable"
	} elseif ($tests_exit -eq 0 -and (Test-Path "$results/$name.totals") -and $o -eq "verbose") {
		Get-Content -Path "$results/$name.totals" | Select-String -AllMatches -Pattern '^object' -CaseSensitive -Raw |
		ForEach-Object {$_.Matches} {
			$line = $_.split("`t")
			Write-Host -NoNewline $mode_prefix
			Write-Host -NoNewline $line[2]
			Write-Host -NoNewline ' tests: '
			Write-Host -NoNewline $line[3]
			Write-Host -NoNewline ' skipped, '
			Write-Host -NoNewline $line[4]
			Write-Host -NoNewline ' passed, '
			Write-Host -NoNewline $line[5]
			Write-Host -NoNewline ' failed ('
			Write-Host -NoNewline $line[6]
			Write-Output ' flaky)'
			Write-Host -NoNewline '%         completed tests from object '
			Write-Host -NoNewline $line[1]
			$duration = Format-Decimal -Number $line[7] -DecimalPlaces 3
			Write-Host -NoNewline ' in '
			Write-Host -NoNewline $duration
			Write-Output ' seconds'
		}
		$coverage_raw = (Get-Content -Path $results/$name.totals | Select-String -Pattern '^coverage' -CaseSensitive -Raw).split("`t")[1]
		if ($coverage_raw -eq "n/a") {
			Write-Output '%         clause coverage n/a'
		} else {
			$coverage_num = [double]($coverage_raw.TrimEnd('%'))
			Write-Host -NoNewline '%         clause coverage '
			Write-Output "$(Format-Decimal -Number $coverage_num -DecimalPlaces 3)%"
		}
	} elseif ($tests_exit -eq 5) {
		if ($o -eq "verbose") {
			Write-Output "%         broken"
		}
		Write-xUnit-Report $unit (Split-Path -Path $unit -Leaf -Resolve) "Broken"
	} elseif ($tests_exit -eq 137 -or $tests_exit -eq 124) {
		if ($o -eq "verbose") {
			Write-Output "%         timeout"
		}
		Add-Content -Path $results/$name.errors -Value "LOGTALK_TIMEOUT"
		Write-xUnit-Report $unit (Split-Path -Path $unit -Leaf -Resolve) "Timeout"
	} elseif ($tests_exit -ne 0) {
		if ($o -eq "verbose") {
			Write-Output "%         crash"
		}
		Add-Content -Path $results/$name.errors -Value "LOGTALK_CRASH"
		Write-xUnit-Report $unit (Split-Path -Path $unit -Leaf -Resolve) "Crash"
	}
	if ($c -eq "xml") {
		if (Test-Path $env:LOGTALKUSER) {
			Copy-Item -Path "$env:LOGTALKUSER/tools/lgtunit/coverage_report.dtd" -Destination .
			Copy-Item -Path "$env:LOGTALKUSER/tools/lgtunit/coverage_report.xsl" -Destination .
		} elseif (Test-Path $env:LOGTALKHOME) {
			Copy-Item -Path "$env:LOGTALKHOME/tools/lgtunit/coverage_report.dtd" -Destination .
			Copy-Item -Path "$env:LOGTALKHOME/tools/lgtunit/coverage_report.xsl" -Destination .
		}
	}
	Pop-Location
}

Function Invoke-Tests() {
param(
	[Parameter(Position = 0)]
	[String]$name,
	[Parameter(Position = 1)]
	[String]$goal
)
	if ($a -eq "") {
		if ($t -ne 0) {
			& $timeout_command $t pwsh (where.exe "$logtalk.ps1") $backend_options $logtalk_option $goal > "$results/$name.results" 2> "$results/$name.errors"
		} else {
			& $logtalk $backend_options $logtalk_option $goal > "$results/$name.results" 2> "$results/$name.errors"
		}
	} else {
		if ($t -ne 0) {
			& $timeout_command $t pwsh (where.exe "$logtalk.ps1") $backend_options $logtalk_option $goal '--' @a > "$results/$name.results" 2> "$results/$name.errors"
		} else {
			& $logtalk $backend_options $logtalk_option $goal '--' @a > "$results/$name.results" 2> "$results/$name.errors"
		}
	}
	if (Select-String -Path "$results/$name.errors" -Pattern "Likely bug in the backend Prolog compiler. Please file a bug report." -SimpleMatch -Quiet) {
		Add-Content -Path "$results/$name.errors" -Value "LOGTALK_BROKEN"
		return 5
	} elseif ($LASTEXITCODE -eq 0 -and
		!(Test-Path "$results/$name.totals" -PathType Leaf) -and
		!(Select-String -Path "$results/$name.results" -Pattern "(not applicable)" -SimpleMatch -Quiet) -and
		!(Select-String -Path "$results/$name.results" -Pattern "tests skipped" -SimpleMatch -Quiet)) {
		Add-Content -Path "$results/$name.errors" -Value "LOGTALK_BROKEN"
		return 5
	} elseif ($LASTEXITCODE -eq 0 -and
		!(Select-String -Path "$results/$name.results" -Pattern "(not applicable)" -SimpleMatch -Quiet) -and
		!(Select-String -Path "$results/$name.totals" -Pattern "^object" -Quiet) -and
		!(Select-String -Path "$results/$name.results" -Pattern "tests skipped" -SimpleMatch -Quiet)) {
		Add-Content -Path "$results/$name.errors" -Value "LOGTALK_BROKEN"
		return 5
	}
	return $LASTEXITCODE
}

Function Write-xUnit-Report() {
param(
	[Parameter(Position = 0)]
	[String]$directory,
	[Parameter(Position = 1)]
	[String]$name,
	[Parameter(Position = 2)]
	[String]$exception
)
	$short = $directory -replace $prefix, ""
	if ($format -eq "xunit") {
		$timestamp=$(Get-Date +"%Y-%m-%dT%H:%M:%S")
		New-Item -Path . -Name "$directory/xunit_report.xml" -ItemType "file" -Force > $null
		Add-Content -Path "$directory/xunit_report.xml" -Value "<?xml version=`"1.0`" encoding=`"UTF-8`"?>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<testsuites>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<testsuite package=`"$short/`" name=`"$short/tests.lgt`" tests=`"0`" errors=`"1`" failures=`"0`" skipped=`"0`" time=`"0`" timestamp=`"$timestamp`" id=`"0`">"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<testcase classname=`"tests`" name=`"$name`" time=`"0`">"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<failure message=`"$exception`" type=`"$exception`">$exception</failure>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "</testcase>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "</testsuite>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "</testsuites>"
	} elseif ($format -eq "xunit_net_v2") {
		$run_date = Get-Date -Format "yyyy-MM-dd"
		$run_time = Get-Date -Format "HH:mm:ss"
		New-Item -Path . -Name "$directory/xunit_report.xml" -ItemType "file" -Force > $null
		Add-Content -Path "$directory/xunit_report.xml" -Value "<?xml version=`"1.0`" encoding=`"UTF-8`"?>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<assemblies>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<assembly name=`"$short/tests.lgt::tests`" config-file=`"$short/tests.lgt`" test-framework=`"lgtunit`" run-date=`"$run_date`" run-time=`"$run_time`" time=`"0`" total=`"0`" errors=`"1`" failures=`"1`" skipped=`"0`">"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<errors>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<error type=`"$exception`" name=`"$exception`">"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<failure exception-type=`"$exception`">"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<message><![CDATA[$exception]]></message>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "</failure>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "</error>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "</errors>"
		# hack for Allure ignoring the "errors" tag
		Add-Content -Path "$directory/xunit_report.xml" -Value "<collection name=`"$short/tests.lgt::tests`" time=`"0`" total=`"1`" passed=`"0`" failed=`"1`" skipped=`"0`">"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<test name=`"$name::tests`" type=`"$short/tests.lgt::tests`" method=`"$name`" time=`"0`" result=`"Fail`">"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<failure exception-type=`"$exception`">"
		Add-Content -Path "$directory/xunit_report.xml" -Value "<message><![CDATA[$exception]]></message>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "</failure>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "</test>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "</collection>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "</assembly>"
		Add-Content -Path "$directory/xunit_report.xml" -Value "</assemblies>"
	} elseif ($format -eq "tap") {
		New-Item -Path . -Name "$directory/tap_report.txt" -ItemType "file" -Force > $null
		Add-Content -Path "$directory/tap_report.txt" -Value "TAP version 13"
		Add-Content -Path "$directory/tap_report.txt" -Value "Bail out! $unit $exception"
	} elseif ($format -eq "ctrf") {
		$timestamp_ms = [int64]([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())
		New-Item -Path . -Name "$directory/ctrf_report.json" -ItemType "file" -Force > $null
		Add-Content -Path "$directory/ctrf_report.json" -Value "{"
		Add-Content -Path "$directory/ctrf_report.json" -Value "  `"reportFormat`": `"CTRF`","
		Add-Content -Path "$directory/ctrf_report.json" -Value "  `"specVersion`": `"1.0.0`","
		Add-Content -Path "$directory/ctrf_report.json" -Value "  `"results`": {"
		Add-Content -Path "$directory/ctrf_report.json" -Value "    `"tool`": {`"name`": `"lgtunit`"},"
		Add-Content -Path "$directory/ctrf_report.json" -Value "    `"summary`": {"
		Add-Content -Path "$directory/ctrf_report.json" -Value "      `"tests`": 1,"
		Add-Content -Path "$directory/ctrf_report.json" -Value "      `"passed`": 0,"
		Add-Content -Path "$directory/ctrf_report.json" -Value "      `"failed`": 1,"
		Add-Content -Path "$directory/ctrf_report.json" -Value "      `"skipped`": 0,"
		Add-Content -Path "$directory/ctrf_report.json" -Value "      `"pending`": 0,"
		Add-Content -Path "$directory/ctrf_report.json" -Value "      `"other`": 0,"
		Add-Content -Path "$directory/ctrf_report.json" -Value "      `"start`": $timestamp_ms,"
		Add-Content -Path "$directory/ctrf_report.json" -Value "      `"stop`": $timestamp_ms,"
		Add-Content -Path "$directory/ctrf_report.json" -Value "      `"duration`": 0"
		Add-Content -Path "$directory/ctrf_report.json" -Value "    },"
		Add-Content -Path "$directory/ctrf_report.json" -Value "    `"tests`": ["
		Add-Content -Path "$directory/ctrf_report.json" -Value "      {`"name`": `"$name`", `"suite`": [`"$short/tests.lgt`"], `"status`": `"failed`", `"duration`": 0, `"filePath`": `"$short/tests.lgt`", `"line`": 0, `"message`": `"$exception`", `"trace`": `"$exception`"}"
		Add-Content -Path "$directory/ctrf_report.json" -Value "    ]"
		Add-Content -Path "$directory/ctrf_report.json" -Value "  }"
		Add-Content -Path "$directory/ctrf_report.json" -Value "}"
	} elseif ($format -eq "subunit_v1") {
		New-Item -Path . -Name "$directory/subunit_v1_report.txt" -ItemType "file" -Force > $null
		Add-Content -Path "$directory/subunit_v1_report.txt" -Value "test: $name [$short/tests.lgt]"
		Add-Content -Path "$directory/subunit_v1_report.txt" -Value "error: $name [$exception]"
	} elseif ($format -eq "subunit_v2") {
		New-Item -Path . -Name "$directory/subunit_v2_report.bin" -ItemType "file" -Force > $null
		Add-Content -Path "$directory/subunit_v2_report.bin" -Value "subunit_v2_error:$($name):$($exception)"
	}
}

Function Write-Usage-Help() {
	$myFullName = $MyInvocation.ScriptName
	$myName = Split-Path -Path "$myFullName" -leaf -Resolve

	Write-Output ""
	Write-Output "This script automates running unit tests found in the current directory and"
	Write-Output "recursively in its sub-directories by scanning by default for `"tester.lgt`""
	Write-Output "and `"tester.logtalk`" source files. In case of failed unit tests or test set"
	Write-Output "errors, this script returns a non-zero exit code. When a `"tester.sh`" file"
	Write-Output "exists in the tests directory, the file is sourced before running the tests."
	Write-Output "The `"tester.sh`" file is sourced with all the parameters passed to the script."
	Write-Output ""
	Write-Output "Usage:"
	Write-Output "  $myName -p prolog [-o output] [-m mode] [-f format] [-d results] [-t timeout] [-j jobs] [-n driver] [-s prefix] [-b tracker] [-u url] [-c report] [-l level] [-e exclude] [-i options] [-g goal] [-r seed] [-w] [-z] [-a arguments]"
	Write-Output "  $myName -v"
	Write-Output "  $myName -h"
	Write-Output ""
	Write-Output "Required arguments:"
	Write-Output "  -p backend Prolog compiler"
	Write-Output "     (valid values are b, cx, eclipse, gnu, gnunc, ji, xvm, sicstus, swi, swipack, tau, trealla, xsb, and yap)"
	Write-Output ""
	Write-Output "Optional arguments:"
	Write-Output "  -o output (valid values are verbose, minimal, and quiet; default is $o)"
	Write-Output "  -m compilation mode (default is $m)"
	Write-Output "     (valid values are optimal, normal, debug, and all)"
	Write-Output "  -f format for writing the test results (default is $f)"
	Write-Output "     (valid values are default, tap, xunit, xunit_net_v2, ctrf, subunit_v1, and subunit_v2)"
	Write-Output "  -d directory to store the test logs (default is $results)"
	Write-Output "  -t timeout in seconds for running each test set (default is $t; i.e. disabled)"
	Write-Output "  -j maximum number of concurrent test set processes (default is $j)"
	Write-Output "  -n name of the test driver and sourced files (minus file name extensions; default is $n)"
	Write-Output "  -s suppress path prefix (default is $prefix)"
	Write-Output "  -b bug report server (valid values are github and gitlab; no default; requires -u option)"
	Write-Output "  -u base URL to generate links to test files (no default)"
	Write-Output "  -c code coverage report (default is $c)"
	Write-Output "     (valid values are none, xml, cobertura, and lcov)"
	Write-Output "  -l directory depth level to look for test sets (default is to recurse into all sub-directories)"
	Write-Output "     (level 1 means current directory only)"
	Write-Output "  -e exclude directories matching a regular expression"
	Write-Output "     (prefix with '^' to include only matching directories)"
	Write-Output "  -i integration script command-line options (no default)"
	Write-Output "  -g initialization goal (default is $initialization_goal)"
	Write-Output "  -r random generator starting seed (no default)"
	Write-Output "  -w wipe default scratch directories (./.lgt_tmp and ./lgt_tmp) before running a test set"
	Write-Output "     (this option should not be used when running parallel processes that use the same test sets)"
	Write-Output "  -z audible completion notification (two chimes on exit 0, five otherwise; default is off)"
	Write-Output "  -a arguments as an array (i.e., comma separated) to be passed to the tests (no default)"
	Write-Output "  -v print version"
	Write-Output "  -h help"
	Write-Output ""
}

Function Confirm-Parameters() {

	if ($v -eq $true) {
		Write-Script-Version
		Exit
	}

	if ($h -eq $true) {
		Write-Usage-Help
		Exit
	}

	if ($p -eq "") {
		Write-Error ("Error! Backend Prolog compiler not specified!")
		Write-Usage-Help
		Exit 1
	} elseif ($p -eq "b") {
		$script:backend = 'b'
		$script:prolog = 'B-Prolog'
		$script:logtalk = "bplgt"
		$script:logtalk_option = "-g"
	} elseif ($p -eq "cx") {
		$script:backend = 'cx'
		$script:prolog = 'CxProlog'
		$script:logtalk = "cxlgt"
		$script:logtalk_option = "--goal"
	} elseif ($p -eq "eclipse") {
		$script:backend = 'eclipse'
		$script:prolog = 'ECLiPSe'
		$script:logtalk = "eclipselgt"
		$script:logtalk_option = "-e"
	} elseif ($p -eq "gnu") {
		$script:backend = 'gnu'
		$script:prolog = 'GNU Prolog'
		$script:logtalk = "gplgt"
		$script:logtalk_option = "--query-goal"
		$env:LINEDIT = "gui=no"
	} elseif ($p -eq "gnunc") {
		$script:backend = 'gnu'
		$script:prolog = 'GNU Prolog (native code)'
		$script:logtalk = "gplgtnc"
		$script:logtalk_option = "--query-goal"
		$env:LINEDIT = "gui=no"
	} elseif ($p -eq "ji") {
		$script:backend = 'ji'
		$script:prolog = 'JIProlog'
		$script:logtalk = "jiplgt"
		$script:logtalk_option = "-n -g"
	} elseif ($p -eq "xvm") {
		$script:backend = 'xvm'
		$script:prolog = 'XVM'
		$script:logtalk = "xvmlgt"
		$script:logtalk_option = "-g"
		$script:dot = "."
	} elseif ($p -eq "sicstus") {
		$script:backend = 'sicstus'
		$script:prolog = 'SICStus Prolog'
		$script:logtalk = "sicstuslgt"
		$script:logtalk_option = "--goal"
		$script:dot = "."
	} elseif ($p -eq "swi") {
		$script:backend = 'swi'
		$script:prolog = 'SWI-Prolog'
		$script:logtalk = "swilgt"
		$script:logtalk_option = "-g"
	} elseif ($p -eq "swipack") {
		$script:backend = 'swipack'
		$script:prolog = 'SWI-Prolog'
		$script:logtalk = "swipl"
		$script:logtalk_option = "-g"
	} elseif ($p -eq "tau") {
		$script:backend = 'tau'
		$script:prolog = 'Tau Prolog'
		$script:logtalk = "taulgt"
		$script:logtalk_option = "-g"
		$script:dot = "."
	} elseif ($p -eq "trealla") {
		$script:backend = 'trealla'
		$script:prolog = 'Trealla Prolog'
		$script:logtalk = "tplgt"
		$script:logtalk_option = "-g"
	} elseif ($p -eq "xsb") {
		$script:backend = 'xsb'
		$script:prolog = 'XSB'
		$script:logtalk = "xsblgt"
		$script:logtalk_option = "-e"
		$script:dot = " ."
	} elseif ($p -eq "yap") {
		$script:backend = 'yap'
		$script:prolog = 'YAP'
		$script:logtalk = "yaplgt"
		$script:logtalk_option = "-g"
	} else {
		Write-Error "Error! Unsupported backend Prolog compiler: $p"
		Write-Usage-Help
		Exit 1
	}

	if ($o -ne "verbose" -and $o -ne "minimal" -and $o -ne "quiet") {
		Write-Error "Error! Unknown output verbosity: $o"
		Write-Usage-Help
		Exit 1
	}

	if ($m -ne "optimal" -and $m -ne "normal" -and $m -ne "debug" -and $m -ne "all") {
		Write-Error "Error! Unknown compilation mode: $m"
		Write-Usage-Help
		Exit 1
	}

	if (-not ($j -match '^[1-9][0-9]*$')) {
		Write-Error "Error! Jobs must be an integer equal or greater than 1: $j"
		Write-Usage-Help
		Exit 1
	}

	if ($f -eq "default") {
		$script:format_goal = $format_default_goal
	} elseif ($f -eq "tap") {
		$script:format_goal = $format_tap_goal
	} elseif ($f -eq "xunit") {
		$script:format_goal = $format_xunit_goal
	} elseif ($f -eq "xunit_net_v2") {
		$script:format_goal = $format_xunit_net_v2_goal
	} elseif ($f -eq "ctrf") {
		$script:format_goal = $format_ctrf_goal
	} elseif ($f -eq "subunit_v1") {
		$script:format_goal = $format_subunit_v1_goal
	} elseif ($f -eq "subunit_v2") {
		$script:format_goal = $format_subunit_v2_goal
	} else {
		Write-Error "Error! Unknown format: $f"
		Write-Usage-Help
		Exit 1
	}

	if ($c -eq "none") {
		$script:coverage_goal = $coverage_default_goal
	} elseif ($c -eq "xml") {
		$script:coverage_goal = $coverage_xml_goal
	} elseif ($c -eq "cobertura") {
		$script:coverage_goal = $coverage_cobertura_goal
	} elseif ($c -eq "lcov") {
		$script:coverage_goal = $coverage_lcov_goal
	} else {
		Write-Error "Error! Unknown coverage report: $c"
		Write-Usage-Help
		Exit 1
	}

	if ($b -ne "") {
		if ($u -eq "") {
			Write-Error "Error! Issue tracker option (-b) requires the base URL (-u) option"
			Write-Usage-Help
			Exit 1
		}
		if ($b.Contains(":")) {
			$script:issue_array = $b.Split(":")
			if ($issue_array.Length -ne 2) {
				Write-Error "Error! Invalid issue tracker format. Expected 'server:labels' or just 'server'"
				Write-Usage-Help
				Exit 1
			}
			$script:issue_server = $issue_array[0]
			$script:issue_labels = $issue_array[1]
		} else {
			$script:issue_server = $b
		}
		if ($script:issue_server -ne "github" -and $script:issue_server -ne "gitlab") {
			Write-Error "Error! Issue tracker server must be either github or gitlab: $b"
			Write-Usage-Help
			Exit 1
		}
	}

	if ($l -ne "") {
		if ($l -match '^[1-9][0-9]*$') {
			$script:level = [int]$l - 1
		} else {
			Write-Error "Error! Level must be an integer equal or greater than 1: $l"
			Write-Usage-Help
			Exit 1
		}
	} else {
		$script:level = 999
	}

	if ($e -ne "") {
		$script:exclude = $e
	}

	if ($i -ne "") {
		$script:backend_options = $i
	}

	if ($g -ne "") {
		$script:initialization_goal = $g
	}

	if ($r -ne "") {
		$script:seed_goal = "logtalk_load(arbitrary(loader)),type::set_seed($r)"
	} else {
		$script:seed_goal = "true"
	}

	if ($d -ne "") {
		$script:results = $d -replace '\\', '/'
	}

	if ($s -ne "") {
		$script:prefix = $s -replace '\\', '/'
	}

	if ($u -ne "" -and -not ($u -match '^https?://')) {
		Write-Error "Error! Base URL must start with http:// or https://"
		Write-Usage-Help
		Exit 1
	}
}

###################### here it starts ############################

$PSNativeCommandArgumentPassing = 'Standard'

Push-Location

$allargs = $PSBoundParameters

# default argument values

$backend = "swi"
$prolog = "SWI-Prolog"
$logtalk = "swilgt"
$logtalk_option = "-g"
$dot = ""
$base = $pwd

$level = 999
$exclude = ""
$include_only = $false

if (Test-Path "$env:Programfiles/Git/usr/bin/timeout.exe") {
	$timeout_command = "$env:Programfiles/Git/usr/bin/timeout.exe"
} else {
	$timeout_command = ""
	Write-Output "Warning! Timeout support not available. The timeout option will be ignored."
}

$flag_goal = "true"
$backend_options = $null
$initialization_goal = "true"

$issue_server = ""
$issue_labels = "bug"

$format_default_goal = "true"
$format_tap_goal = "logtalk_load(lgtunit(tap_report))"
$format_xunit_goal = "logtalk_load(lgtunit(xunit_report))"
$format_xunit_net_v2_goal = "logtalk_load(lgtunit(xunit_net_v2_report))"
$format_ctrf_goal = "logtalk_load(lgtunit(ctrf_report))"
$format_subunit_v1_goal = "logtalk_load(lgtunit(subunit_v1_report))"
$format_subunit_v2_goal = "logtalk_load(lgtunit(subunit_v2_report))"
$format_goal = $format_default_goal

$coverage_default_goal = "true"
$coverage_xml_goal = "logtalk_load(lgtunit(coverage_report))"
$coverage_cobertura_goal = "logtalk_load(lgtunit(cobertura_report))"
$coverage_lcov_goal = "logtalk_load(lgtunit(lcov_report))"
$coverage_goal = $coverage_default_goal

$results = (Join-Path $pwd "logtalk_tester_logs") -replace '\\', '/'
$prefix = "$env:USERPROFILE/" -replace '\\', '/'

Confirm-Parameters

$max_jobs = [int]$j

if ($p -eq "swipack") {
	$initialization_goal = "use_module(library(logtalk)),$initialization_goal"
}

$versions_goal = "logtalk_load(library(tester_versions)),halt$dot"

$tester_optimal_goal = "set_logtalk_flag(optimize,on),logtalk_load($n),halt$dot"
$tester_normal_goal = "logtalk_load($n),halt$dot"
$tester_debug_goal = "set_logtalk_flag(debug,on),logtalk_load($n),halt$dot"

if ($testset -ne "") {
	Invoke-TestSet $testset
	Exit 0
}

try {
	New-Item -Path $results -ItemType directory -Force > $null
} catch {
	Write-Error "Error! Failed to create results directory: $_"
	Exit 1
}

if (Test-Path "$results/*.results") {
	Remove-Item -Path "$results/*.results" -Force
}
if (Test-Path "$results/*.errors") {
	Remove-Item -Path "$results/*.errors" -Force
}
if (Test-Path "$results/*.totals") {
	Remove-Item -Path "$results/*.totals" -Force
}
if (Test-Path "$results/errors.all") {
	Remove-Item -Path "$results/errors.all" -Force
}
if (Test-Path "$results/tester_versions.txt") {
	Remove-Item -Path "$results/tester_versions.txt" -Force
}

$start_time = Get-Date

if ($o -ne "quiet") {
	$start_date = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
	Write-Output "% Batch testing started @ $start_date"
	& $logtalk $backend_options $logtalk_option $versions_goal | Out-File $results/tester_versions.txt
	Select-String -Path $results/tester_versions.txt -Pattern "Logtalk version:" -Raw -SimpleMatch
	(Select-String -Path $results/tester_versions.txt -Pattern "Prolog version:" -Raw -SimpleMatch) -replace "Prolog", $prolog
	Select-String -Path $results/tester_versions.txt -Pattern "OS version:" -Raw -SimpleMatch
	Write-Output "%"
}

if ($l -eq "") {
	$driver_files = Get-ChildItem -Path "$base/*" -Include "$n.lgt", "$n.logtalk" -Recurse
} else {
	$driver_files = Get-ChildItem -Path "$base/*" -Include "$n.lgt", "$n.logtalk" -Depth $level
}

if ($exclude -ne "") {
	if ($exclude.StartsWith('^')) {
		$include_only = $true
		$exclude = $exclude.Substring(1)
	} else {
		$include_only = $false
	}
	if ($include_only) {
		$driver_files = $driver_files | Where-Object {$_.FullName -match $exclude}
	} else {
		$driver_files = $driver_files | Where-Object {$_.FullName -notmatch $exclude}
	}
}

$driver_files = $driver_files | Sort-Object -Property FullName
$testsets = ($driver_files | Measure-Object).Count

if ($testsets -eq 0) {
	if ($o -ne "quiet") {
		Write-Output "% 0 test sets: 0 completed, 0 skipped, 0 broken, 0 timedout, 0 crashed"
		Write-Output "% 0 tests: 0 skipped, 0 passed, 0 failed"
	}
	Exit 0
}

if ($max_jobs -eq 1) {
	if ($o -eq "verbose") {
		$driver_files | ForEach-Object {
			Invoke-TestSet $_.FullName
			Write-Output "%"
		}
	} elseif ($o -eq "minimal") {
		$counter = 1
		$driver_files | ForEach-Object {
			Write-Host -NoNewline "% Running $testsets test sets: "
			Write-Host -NoNewline "$counter`r"
			Invoke-TestSet $_.FullName
			$counter++
		}
		Write-Output "%"
	} else {
		$driver_files | ForEach-Object {
			Invoke-TestSet $_.FullName
		}
	}
} else {
	$worker_arguments = @('-NoProfile', '-File', $PSCommandPath, '-p', $p, '-o', $o, '-m', $m, '-f', $f, '-d', $results, '-t', $t, '-j', '1', '-n', $n, '-c', $c)
	if ($s -ne "") { $worker_arguments += @('-s', $s) }
	if ($b -ne "") { $worker_arguments += @('-b', $b) }
	if ($u -ne "") { $worker_arguments += @('-u', $u) }
	if ($l -ne "") { $worker_arguments += @('-l', $l) }
	if ($e -ne "") { $worker_arguments += @('-e', $e) }
	if ($i -ne "") { $worker_arguments += @('-i', $i) }
	if ($g -ne "") { $worker_arguments += @('-g', $g) }
	if ($r -ne "") { $worker_arguments += @('-r', $r) }
	if ($w -eq $true) { $worker_arguments += '-w' }
	if ($z -eq $true) { $worker_arguments += '-z' }
	if ($null -ne $a -and $a.Count -gt 0) { $worker_arguments += @('-a', ($a -join ',')) }

	$workers = @()
	$completed = 0
	$counter = 1
	$pwsh_executable = (Get-Command pwsh).Source

	# Start-Process resolves a bare 'NUL' as a relative path rather than the
	# reserved device name, so use a real empty file as the workers' stdin
	# instead. This keeps each worker from sharing the parent console's
	# input handle, which is what causes progress/output updates to stall
	# until the user presses Enter.
	$stdin_null_file = Join-Path $results '.stdin_null'
	if (-not (Test-Path -LiteralPath $stdin_null_file)) {
		New-Item -ItemType File -Path $stdin_null_file -Force | Out-Null
	}

	$reap_completed_workers = {
		for ($i = 0; $i -lt $workers.Count; $i++) {
			$worker = $workers[$i]
			if ($worker.Done) {
				continue
			}

			if ($worker.Process.HasExited) {
				$error_file = "$results/$($worker.Name).console.err"
				if ($worker.Process.ExitCode -ne 0) {
					$error_path = "$results/$($worker.Name).errors"
					if (!(Test-Path $error_path) -or !(Select-String -Path $error_path -Pattern 'LOGTALK_TIMEOUT|LOGTALK_CRASH|LOGTALK_BROKEN' -Quiet)) {
						Add-Content -Path $error_path -Value "LOGTALK_CRASH"
					}
				}

				if ($o -eq 'verbose') {
					$console_file = "$results/$($worker.Name).console"
					if (Test-Path $console_file) {
						Get-Content -Path $console_file
					}
					if (Test-Path $error_file) {
						Get-Content -Path $error_file
					}
					if ((Test-Path $console_file) -or (Test-Path $error_file)) {
						Write-Output "%"
					}
				}

				$workers[$i].Done = $true
				$script:completed++

				if ($o -eq 'minimal') {
					Write-Host -NoNewline "% Running $testsets test sets: "
					Write-Host -NoNewline "$completed`r"
				}
			}
		}
	}

	try {
		$driver_files | ForEach-Object {
			while ((($workers | Where-Object { -not $_.Done -and -not $_.Process.HasExited }) | Measure-Object).Count -ge $max_jobs) {
				& $reap_completed_workers
				Start-Sleep -Milliseconds 100
			}
			& $reap_completed_workers
			if ($o -eq 'minimal') {
				Write-Host -NoNewline "% Running $testsets test sets: "
				Write-Host -NoNewline "$counter`r"
			}
			$unit = (Split-Path -Path $_.FullName) -replace '\\', '/'
			$name = (($unit -replace '/', '__') -replace '\\', '__') -replace ':', '___'
			$console_file = "$results/$name.console"
			$error_file = "$results/$name.console.err"
			$arguments = @($worker_arguments + @('-testset', $_.FullName))
			$argument_string = ConvertTo-ArgumentString -Arguments $arguments
			$process = Start-Process -FilePath $pwsh_executable -ArgumentList $argument_string -NoNewWindow -RedirectStandardOutput $console_file -RedirectStandardError $error_file -RedirectStandardInput $stdin_null_file -PassThru
			$workers += [PSCustomObject]@{ Process = $process; Name = $name; Done = $false }
			$counter++
		}

		while ($completed -lt $testsets) {
			& $reap_completed_workers
			Start-Sleep -Milliseconds 50
		}
	} finally {
		foreach ($worker in $workers) {
			if (-not $worker.Process.HasExited) {
				try {
					$worker.Process.Kill()
				} catch {
					# process may have exited between the check and the kill; nothing to do
				}
			}
		}
	}

	if ($o -eq 'minimal') {
		Write-Output ""
	}

	Remove-Item -LiteralPath $stdin_null_file -Force -ErrorAction SilentlyContinue

	Get-ChildItem -Path $results -Filter '*.console' -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
	Get-ChildItem -Path $results -Filter '*.console.err' -ErrorAction SilentlyContinue |
		Where-Object {
			$error_path = ($_.FullName -replace '\.console\.err$', '.errors')
			!(Test-Path $error_path)
		} |
		Remove-Item -Force -ErrorAction SilentlyContinue
}

Push-Location $results

$testsetskipped = (Get-ChildItem -Path . -Filter *.results | Select-String -Pattern 'tests skipped').count + (Get-ChildItem -Path . -Filter *.results | Select-String -Pattern '(not applicable)').count

$timeouts = (Get-ChildItem -Path . -Filter *.errors | Select-String -Pattern 'LOGTALK_TIMEOUT').count
$crashed =  (Get-ChildItem -Path . -Filter *.errors | Select-String -Pattern 'LOGTALK_CRASH').count
$broken =   (Get-ChildItem -Path . -Filter *.errors | Select-String -Pattern 'LOGTALK_BROKEN').count

$testsetruns = $testsets - $testsetskipped - $timeouts - $crashed - $broken


$skipped = 0
$passed = 0
$failed = 0
$flaky = 0

Get-ChildItem -Path . -Filter *.totals |
Foreach-Object {
	Get-Content -Path $_ |
	Select-String -Pattern '^object' -CaseSensitive |
	ForEach-Object {
		$line = $_.Line.Split("`t")
		$skipped = $skipped + [int]$line[3]
		$passed =  $passed  + [int]$line[4]
		$failed =  $failed  + [int]$line[5]
		$flaky =   $flaky   + [int]$line[6]
	}
}

$total = $skipped + $passed + $failed

$quiet_report = {
	if ($o -eq "minimal") {
		Write-Output "%"
	}
	if ((Get-ChildItem -Path . -Filter *.errors  | Get-Content | Select-String -Pattern '^!' -CaseSensitive -Quiet) -or
		(Get-ChildItem -Path . -Filter *.results | Get-Content | Select-String -Pattern '^!' -CaseSensitive -Quiet) -or
		(Get-ChildItem -Path . -Filter *.errors  | Get-Content | Select-String -Pattern '^\*' -CaseSensitive -Quiet) -or
		(Get-ChildItem -Path . -Filter *.results | Get-Content | Select-String -Pattern '^\*' -CaseSensitive -Quiet)) {
		Write-Output "% Compilation errors/warnings and failed unit tests"
		Write-Output "% (compilation errors/warnings might be expected depending on the test)"
		Write-Output "%"
		Get-ChildItem -Path . -Filter *.errors  | Get-Content | Select-String -Pattern '^!'  -NoEmphasis | ForEach-Object { $_.Line } | Tee-Object -FilePath errors.all -Append
		Get-ChildItem -Path . -Filter *.results | Get-Content | Select-String -Pattern '^!'  -NoEmphasis | ForEach-Object { $_.Line } | Tee-Object -FilePath errors.all -Append
		Get-ChildItem -Path . -Filter *.errors  | Get-Content | Select-String -Pattern '^\*' -NoEmphasis | ForEach-Object { $_.Line } | Tee-Object -FilePath errors.all -Append
		Get-ChildItem -Path . -Filter *.results | Get-Content | Select-String -Pattern '^\*' -NoEmphasis | ForEach-Object { $_.Line } | Tee-Object -FilePath errors.all -Append
	}
	if ((Get-ChildItem -Path . -Filter *.results | Get-Content | Select-String -Pattern 'tests skipped' -CaseSensitive -SimpleMatch -Quiet) -or
		(Get-ChildItem -Path . -Filter *.results | Get-Content | Select-String -Pattern '(not applicable)' -CaseSensitive -SimpleMatch -Quiet)) {
		Write-Output "% Skipped test sets"
		Get-ChildItem -Path . -Filter *.results |
		Foreach-Object {
			if (Get-Content -Path $_ | Select-String -Pattern 'tests skipped' -SimpleMatch -Quiet) {
				($_.BaseName -replace '__', '/') -replace $prefix, ""
			}
			if (Get-Content -Path $_ | Select-String -Pattern '(not applicable)' -SimpleMatch -Quiet) {
				($_.BaseName -replace '__', '/') -replace $prefix, ""
			}
		}
		Write-Output "%"
	}
	if (Get-ChildItem -Path . -Filter *.errors | Get-Content | Select-String -Pattern 'LOGTALK_BROKEN' -CaseSensitive -SimpleMatch -Quiet) {
		Write-Output "% Broken"
		Get-ChildItem -Path . -Filter *.errors |
		Foreach-Object {
			if (Select-String -Path $_.FullName -Pattern 'LOGTALK_BROKEN' -CaseSensitive -SimpleMatch -Quiet) {
				(($_.BaseName -replace '___', ':') -replace '__', '/') -replace $prefix, ""
			}
		}
		Write-Output "%"
	}
	if (Get-ChildItem -Path . -Filter *.errors | Get-Content | Select-String -Pattern 'LOGTALK_TIMEOUT' -SimpleMatch -CaseSensitive -Quiet) {
		Write-Output "% Timedout"
		Get-ChildItem -Path . -Filter *.errors |
		Foreach-Object {
			if (Select-String -Path $_.FullName -Pattern 'LOGTALK_TIMEOUT' -CaseSensitive -SimpleMatch -Quiet) {
				(($_.BaseName -replace '___', ':') -replace '__', '/') -replace $prefix, ""
			}
		}
		Write-Output "%"
	}
	if (Get-ChildItem -Path . -Filter *.errors | Get-Content | Select-String -Pattern 'LOGTALK_CRASH' -CaseSensitive -SimpleMatch -Quiet) {
		Write-Output "% Crashed"
		Get-ChildItem -Path . -Filter *.errors |
		Foreach-Object {
			if (Select-String -Path $_.FullName -Pattern 'LOGTALK_CRASH' -CaseSensitive -SimpleMatch -Quiet) {
				(($_.BaseName -replace '___', ':') -replace '__', '/') -replace $prefix, ""
			}
		}
		Write-Output "%"
	}
	if (Get-ChildItem -Path . -Filter *.totals | Get-Content | Select-String -Pattern '^skipped' -CaseSensitive -Quiet) {
		Write-Output "% Skipped tests"
		Get-ChildItem -Path . -Filter *.totals |
		Foreach-Object {
			Get-Content -Path $_ |
			Foreach-Object {
				if ($_ -match '^skipped') {
					($_.split("`t")[1] + " - " + $_.split("`t")[2]) -replace $prefix, ""
				}
			}
		}
		Write-Output "%"
	}
	if (Get-ChildItem -Path . -Filter *.totals | Get-Content | Select-String -Pattern '^failed' -CaseSensitive -Quiet) {
		Write-Output "% Failed tests"
		Get-ChildItem -Path . -Filter *.totals |
		Foreach-Object {
			Get-Content -Path $_ |
			Foreach-Object {
				if ($_ -match '^failed') {
					($_.split("`t")[1] + " - " + $_.split("`t")[2]) -replace $prefix, ""
				}
			}
		}
		Write-Output "%"
	}
	Write-Output "% $testsets test sets: $testsetruns completed, $testsetskipped skipped, $broken broken, $timeouts timedout, $crashed crashed"
	Write-Output "% $total tests: $skipped skipped, $passed passed, $failed failed ($flaky flaky)"

	$runtime = (Get-Date) - $start_time
	$runtime_str = "{0}h:{1:D2}m:{2:D2}s" -f [int][Math]::Floor($runtime.TotalHours), $runtime.Minutes, $runtime.Seconds

	if ($o -eq "verbose") {
		$end_date = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
		Write-Output "%"
		Write-Output "% Batch testing ended @ $end_date"
	}

	Write-Output "% Batch run took $runtime_str"
}

if ($o -eq "quiet") {
	& $quiet_report | Out-Null
} else {
	& $quiet_report
}

Pop-Location

if ($crashed -gt 0) {
	$exit_code = 7
} elseif ($broken -gt 0) {
	$exit_code = 5
} elseif ($timeouts -gt 0) {
	$exit_code = 3
} elseif (($failed - $flaky) -gt 0) {
	$exit_code = 1
} else {
	$exit_code = 0
}

Invoke-CompletionNotification $exit_code
Exit $exit_code
