testtools/TestModule.psm1

#Requires -Module Pester, git-completion

using namespace System.Management.Automation;

$ErrorActionPreference = 'Continue'

. "$PSScriptRoot/ConvertCompletion.ps1"

$script:NoOptionsCompletion = '--no-...' | ConvertTo-Completion -ResultType Text -CompletionText '--no-'

function ConvertTo-KebabCase {
    param (
        [Parameter(ValueFromPipeline)]
        [string]$Text
    )
    process {
        ($Text -creplace '(?!^)([A-Z])', '-$1').ToLowerInvariant()
    }
}

function Initialize-Home {
    $script:GitCompletionSettings = @{
        IgnoreCase         = $false;
        ShowAllCommand     = $false;
        ShowAllOptions     = $false;
        CheckoutNoGuess    = $false
        AdditionalCommands = @();
        ExcludeCommands    = @();
    }

    $script:envHOMEBak = $env:HOME

    mkdir ($env:HOME = "$TestDrive/home")
    "[user]`nemail = Kitazato@example.com`nname = 1000yen" | Out-File "$env:HOME/.gitconfig" -Encoding ascii
}
function Restore-Home {
    $env:HOME = $script:envHOMEBak
}

function Initialize-SimpleRepo {
    [CmdletBinding(PositionalBinding)]
    param(
        $rootPath
    )

    Push-Location $rootPath
    git init --initial-branch=main
    git commit -m "initial" --allow-empty
    mkdir Pwsh | Out-Null
    'Pwsh/ign*' > 'Pwsh/ignored'
    'Pwsh/ign*' | Out-File '.gitignore' -Encoding ascii
    'echo world' > 'Pwsh/world.ps1'
    git add -A 2>$null
    git commit -m "World"
    git config alias.sw "switch"
    git config alias.swf "sw -f"
    git config --file test.config alias.ll "!ls"
    Pop-Location
}
function Initialize-Remote {
    [CmdletBinding(PositionalBinding)]
    param(
        $rootPath,
        $remotePath
    )

    Push-Location $remotePath
    git init --initial-branch=develop
    "Initial" > 'initial.txt'
    "echo hello" > 'hello.sh'
    git update-index --add --chmod=+x hello.sh
    git add -A 2>$null
    git commit -m "initial"
    (0..10) > Number.txt
    git add Number.txt
    git commit -m zeta
    git tag zeta
    git reset HEAD~ --hard
    Pop-Location

    Push-Location $rootPath
    git init --initial-branch=main

    git remote add origin "$remotePath"
    git remote add ordinary "$remotePath"
    git remote add grm "$remotePath"

    git config set remotes.default "origin grm"
    git config set remotes.ors "origin ordinary"

    git fetch origin --tags 2>$null
    git fetch ordinary 2>$null
    git fetch grm 2>$null
    git reset origin/develop --hard 2>$null
    git tag initial
    mkdir Pwsh

    'Pwsh/ign*' > 'Pwsh/ignored'
    'Pwsh/ign*' | Out-File '.gitignore' -Encoding ascii
    'echo world' > 'Pwsh/world.ps1'
    git add -A 2>$null
    git commit -m "World"
    Pop-Location
}

function Initialize-FilesRepo {
    [CmdletBinding(PositionalBinding)]
    param(
        $rootPath,
        $remotePath
    )

    if ($remotePath) {
        Initialize-Remote $rootPath $remotePath
    }
    else {
        Initialize-SimpleRepo $rootPath
    }

    Push-Location $rootPath
    New-Item "$TestDrive/gitRoot/Pwsh/OptionLike/-foo.ps1" -ItemType File -Force
    New-Item "$TestDrive/gitRoot/Dr.Wily" -ItemType File
    New-Item "$TestDrive/gitRoot/Aquarion Evol" -ItemType Directory
    New-Item "$TestDrive/gitRoot/Aquarion Evol/Evol" -ItemType File
    New-Item "$TestDrive/gitRoot/Aquarion Evol/Ancient" -ItemType Directory
    New-Item "$TestDrive/gitRoot/Aquarion Evol/Ancient/Soler" -ItemType File
    git add "$TestDrive/gitRoot/Dr.Wily" "$TestDrive/gitRoot/Aquarion Evol/" "$TestDrive/gitRoot/Pwsh/OptionLike/-foo.ps1"
    git commit -m "Start"
    'X' > "$TestDrive/gitRoot/Dr.Wily"
    'LOVE' > "$TestDrive/gitRoot/Aquarion Evol/Evol"
    '-bar' > "$TestDrive/gitRoot/Pwsh/OptionLike/-foo.ps1"

    New-Item "$TestDrive/gitRoot/Pwsh/L1/L2/🏪.ps1" -ItemType File -Force
    New-Item "$TestDrive/gitRoot/漢字" -ItemType Directory
    New-Item "$TestDrive/gitRoot/漢``'帝 国'" -ItemType File
    New-Item "$TestDrive/gitRoot/Deava" -ItemType File
    New-Item "$TestDrive/gitRoot/Aquarion Evol/Gepard" -ItemType File
    New-Item "$TestDrive/gitRoot/Aquarion Evol/Gepada" -ItemType File
    Pop-Location
}

function Complete-FromLine {
    [OutputType([CompletionResult[]])]
    param (
        [string][Parameter(ValueFromPipeline)] $line,
        [string]$Right = ''
    )

    switch -Wildcard ($line) {
        'gitk *' {
            Set-Alias Complete Complete-Gitk -Scope Local
            break
        }
        'git *' {
            Set-Alias Complete Complete-Git -Scope Local
            break
        }
        Default { throw 'Invalid input' }
    }

    $CommandAst = [System.Management.Automation.Language.Parser]::ParseInput($line + $Right, [ref]$null, [ref]$null).EndBlock.Statements.PipelineElements
    $CursorPosition = $line.Length
    return (Complete -CommandAst $CommandAst -CursorPosition $CursorPosition)
}

function writeObjectLine {
    param(
        [Parameter(Position = 0)]$Completion,
        [string] $Prefix = '',
        [string] $Suffix = ''
    )

    "$Prefix$(if($Completion){[PSCustomObject]@{
        CompletionText = $Completion.CompletionText;
        ListItemText = $Completion.ListItemText;
        ResultType = $Completion.ResultType;
        ToolTip = $Completion.ToolTip;
    }})$Suffix"

}

function buildFailedMessage {
    [OutputType([string[]])]
    param (
        $ActualValue,
        [array] $ExpectedValue
    )

    if (!$ActualValue) {
        if (!$ExpectedValue) {
            return @()
        }
        "The expected collection is not empty, but the resulting collection is empty."
        "Expected:"
        foreach ($e in $ExpectedValue) {
            writeObjectLine $e -Suffix ','
        }
        return
    }
    elseif ($null -eq $ExpectedValue) {
        "The expected collection is null."
    }

    if ($ActualValue.Count -ne $ExpectedValue.Count) {
        "The expected collection with size $($ExpectedValue.Count), but the resulting collection with size $($ActualValue.Count)."
    }

    $Length = [math]::Max($ExpectedValue.Length, $ActualValue.Length)
    for ($i = 0; $i -lt $Length ; $i++) {
        $a = [CompletionResult]$ActualValue[$i]
        $e = $ExpectedValue[$i]

        if (
            ($a.CompletionText -ne $e.CompletionText) -or 
            ($a.ListItemText -ne $e.ListItemText) -or 
            ($a.ToolTip -ne $e.ToolTip) -or 
            ($a.ResultType -ne [System.Management.Automation.CompletionResultType]$e.ResultType)
        ) {
            $head = "At index:$i,expected "
            $second = "but actual "
            $second = ' ' * ($head.Length - $second.Length) + $second

            writeObjectLine $e -Prefix $head
            writeObjectLine $a -Prefix $second
        }
    }
}

function Should-BeCompletion {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseApprovedVerbs', '', Scope = 'Function')]
    param(
        $ActualValue,
        [array] $ExpectedValue,
        [string] $Because
    ) 
    <#
    .SYNOPSIS
        Asserts if collection equals expected completion
    #>


    $message = @(buildFailedMessage -ActualValue $ActualValue -ExpectedValue $ExpectedValue)

    if ($message) {
        if ($Because) {
            $message += "because $Because"
        }
        return [PSCustomObject]@{
            Succeeded      = $false
            FailureMessage = $message -join "`n"
        }
    }

    return [PSCustomObject]@{
        Succeeded      = $true
        FailureMessage = $null
    }
}

Add-ShouldOperator -Name BeCompletion `
    -Test ${function:Should-BeCompletion} `
    -SupportsArrayInput

Export-ModuleMember `
    -Function Complete-FromLine, Initialize-Home, Restore-Home, `
    ConvertTo-KebabCase, ConvertTo-Completion, `
    Initialize-Remote, Initialize-SimpleRepo, Initialize-FilesRepo `
    -Variable 'NoOptionsCompletion'