configmap.tests.ps1

BeforeDiscovery {
    . "$PSScriptRoot\test-utils.ps1"
}
BeforeAll {
    Get-Module ConfigMap -ErrorAction SilentlyContinue | Remove-Module
    Import-Module $PSScriptRoot\configmap.psm1

    
    function Get-ValuesList(
        [ValidateScript({
                $_ -is [System.Collections.IDictionary] -and $_.options
            })]
        $map
    ) {
        if (!$map.options) {
            throw "map doesn't have 'options' entry"
        }

        return Get-CompletionList $map.options -reservedKeys $language.reservedKeys
    }

}

Describe "Test-IsParentEntry" {
    It "should identify scriptblock as leaf" {
        $entry = { Write-Host "Command" }
        $result = Test-IsParentEntry $entry
        $result.IsParent | Should -Be $false
        $result.HasExplicitList | Should -Be $false
    }

    It "should identify command object with exec as leaf" {
        $entry = @{
            exec        = { Write-Host "Command" }
            description = "A command"
        }
        $result = Test-IsParentEntry $entry
        $result.IsParent | Should -Be $false
        $result.HasExplicitList | Should -Be $false
    }

    It "should identify explicit list as parent" {
        $entry = @{
            list = @{
                "cmd1" = { Write-Host "Command 1" }
                "cmd2" = { Write-Host "Command 2" }
            }
        }
        $result = Test-IsParentEntry $entry
        $result.IsParent | Should -Be $true
        $result.HasExplicitList | Should -Be $true
    }

    It "should identify direct nested structure as parent" {
        $entry = @{
            "subcmd1" = { Write-Host "Sub command 1" }
            "subcmd2" = @{
                exec = { Write-Host "Sub command 2" }
            }
        }
        $result = Test-IsParentEntry $entry
        $result.IsParent | Should -Be $true
        $result.HasExplicitList | Should -Be $false
    }

    It "should identify data object as leaf" {
        $entry = @{
            name  = "test"
            value = 42
            items = @("a", "b", "c")
        }
        $result = Test-IsParentEntry $entry
        $result.IsParent | Should -Be $false
        $result.HasExplicitList | Should -Be $false
    }

    It "should identify mixed command object as parent when exec and subcommand is present" {
        $entry = @{
            exec     = { Write-Host "Main command" }
            "subcmd" = { Write-Host "This should not make it a parent" }
        }
        $result = Test-IsParentEntry $entry
        $result.IsParent | Should -Be $true
        $result.HasExplicitList | Should -Be $false
    }
}


Describe "map parsing" {
    BeforeAll {
        $language = Get-MapLanguage "conf"
    }
    Describe '<name>' -ForEach @(
        @{
            Name = "simple list"
            Map  = @("item1", "item2")
            Keys = @("item1", "item2")
        }
        @{
            Name = "simple map"
            Map  = [ordered]@{
                "key1" = @{ id = "a" }
                "key2" = @{ id = "b" }
            }
            Keys = @("key1", "key2")
        }
        @{
            Name    = "one-level simple list"
            Map     = [ordered]@{
                "key1" = @{
                    list = ("a", "b")
                }
                "key2" = @{ id = "b" }
            }
            Flatten = @("key1*", "a", "b", "key2")
            Tree    = @("key1.a", "key1.b", "key2")
        }
        @{
            Name    = "config-like"
            Map     = [ordered]@{
                "db"      = @{
                    options = @{
                        "local"  = @{
                            connectionString = "blah"
                        }
                        "remote" = @{
                            connectionString = "boom"
                        }
                    }
                }
                "secrets" = @{
                    list = [ordered]@{
                        connectionString = @{
                            "local"  = "blah"
                            "remote" = "boom"
                        }
                        keyVault         = @{
                            "local"  = "blah"
                            "remote" = "boom"
                        }
                    }
                }
            }
            Flatten = @("db", "secrets*", "connectionString", "keyVault")
            Tree    = @("db", "secrets.connectionString", "secrets.keyVault")
        }
    ) {
        It '<name> => flatten keys' {
            $list = (Get-CompletionList -map $map -flatten:$true -reservedKeys $language.reservedKeys)
            if (!$flatten) {
                $flatten = $keys
            }

            $list.Keys | Should -Be $Flatten
        }
        It '<name> => tree keys' {
            $list = (Get-CompletionList -map $map -flatten:$false -leafsOnly:$true -reservedKeys $language.reservedKeys)
            if (!$tree) {
                $tree = $keys
            }

            $list.Keys | Should -Be $Tree
        }
    }

    Describe "values" -ForEach @(
        @{
            Name   = "options value"
            Map    = @{
                options = [ordered]@{
                    "a" = 1
                    "b" = 2
                }
            }
            Values = @("a", "b")
        }
        @{
            Name   = "options func"
            Map    = @{
                options = {
                    return [ordered]@{
                        "a" = 1
                        "b" = 2
                    }
                }
            }
            Values = @("a", "b")
        }
    ) {
        Describe "<name>" {
            It "<name> => options" {
                $result = Get-ValuesList $map
                $result.Keys | Should -Be $Values
            }
        }
    }
}

Describe "map execuction" {
    BeforeEach {
        function exec-mock($_context) { "real" }
        Mock exec-mock { param($_context) Write-Host $_context }
    }
    Describe 'exec without args' -ForEach @(
        @{
            Name = "simple scriptblock"
            Map  = @{
                "build" = {
                    param($_context)
                    exec-mock
                }
            }
        }
    ) {
        It "<name> => exec-mock without args" {
            $entry = Get-MapEntry $map "build"
            $s = Get-EntryCommand $entry
            $p = Get-ScriptArgs $s

            Invoke-EntryCommand $entry "build" -context @{ a = 1 }
            Should -Invoke exec-mock
        }
    }
    Describe 'exec with args' -ForEach @(
        @{
            Name = "scriptblock with param"
            Map  = @{
                "build" = {
                    param($_context)

                    exec-mock $_context
                }
            }
        }
    ) {
        It "<name> => exec-mock" {
            $result = Get-CompletionList $map

            Invoke-EntryCommand $result.build -bound @{ _context = @{ a = 1 } }
            Should -Invoke exec-mock -ParameterFilter {
                $_context | Should -MatchObject @{ a = 1 }
                return $true
            }
        }
    }
}

Describe "qbuild" {
    BeforeAll {
        function Invoke-Build {
            param($ctx, [bool][switch]$noRestore)
        }
        Mock Invoke-Build {
            param($ctx, [bool][switch]$noRestore)

            $bound = $PSBoundParameters
            Write-Host "build script body"
            Write-Host "ctx=$($ctx | ConvertTo-Json)"
            Write-Host "noRestore=$noRestore"
            Write-Host "bound=$($bound | ConvertTo-Json)"
        }
        $targets = @{
            "build" = {
                param($ctx, [bool][switch]$noRestore)

                Invoke-Build @PSBoundParameters
            }
        }
    }

    Describe "script custom parameters" {
        It "should return parameters" {

            $parameters = Get-ScriptArgs $targets.build
            $parameters.Keys | Should -Be @("ctx", "noRestore")
        }

        It "should invoke with correct parameters" {
            qbuild -map $targets "build" -NoRestore
            Should -Invoke Invoke-Build -Times 1 -ParameterFilter { $noRestore -eq $true }
        }
    }
}

Describe "qconf" {
    BeforeAll {
        function Set-Conf {
            param($key, $value)
        }
        Mock Set-Conf
        $targets = [ordered]@{
            "db" = @{
                options = { return [ordered]@{
                        "local"  = @{
                            "connectionString" = "localconnstr"
                        }
                        "remote" = @{
                            "connectionString" = "localconnstr"
                        }
                    }
                }
                set     = {
                    param($key, $value)

                    Set-Conf @PSBoundParameters
                }
                get     = {
                    return "my_value"
                }
            }
            "test" = @{
                get = {
                    return "test_value"
                }
            }
        }

        $language = Get-MapLanguage "conf"
    }

    Describe "set custom parameters" {
        It "should return parameters" {
            $parameters = Get-ScriptArgs $targets.db.set
            $parameters.Keys | Should -Be @("key", "value")
        }
        It "should return top-level completion list" {
            $list = Get-CompletionList $targets -reservedKeys $language.reservedKeys
            $list.Keys | Should -Be @("db","test")
        }
        It "should return options list" {
            $entry = Get-MapEntry $targets "db"
            $entry | Should -Not -BeNullOrEmpty
            $options = Get-CompletionList $entry -listKey "options" -reservedKeys $language.reservedKeys
            $options.Keys | Should -Be @("local", "remote")
        }
        It "invoke options" {
            $r = Invoke-EntryCommand $targets.db "options"
            $r.Keys | Should -Be @("local", "remote")
        }
        It "invoke get" {
            $r = Invoke-EntryCommand $targets.db "get"
            $r | Should -Be "my_value"
        }
        It "invoke set" {
            $r = Invoke-EntryCommand $targets.db "set" -bound @{ "key" = "key1"; "value" = "value2" }
            Should -Invoke Set-Conf -ParameterFilter { $key -eq "key1" -and $value -eq "value2" }
        }
        It "qconf get without entry should return list of all values" {
            $result = qconf -map $targets "get"
            $result | Should -Not -BeNullOrEmpty
            $result | Should -HaveCount 2
            $result[0].Path | Should -Be "db/"
            $result[1].Path | Should -Be "test/"
        }
    }
}

Describe "unified" {
    BeforeAll {

        Mock Write-Host

        $targets = @{
            "write:simple"  = {
                param([string] $message)

                Write-Host "SIMPLE: '$message'"
            }
            "write:wrapped" = @{
                exec  = {
                    param([string] $message)

                    Write-Host "WRAPPED: '$message'"
                }

                other = {
                    param([string] $message)

                    Write-Host "OTHER: '$message'"
                }
            }
            "write:custom"  = @{
                go = {
                    param([string] $message)
                    return "CUSTOM: '$message'"
                }
            }
            "write:getset"  = @{
                go  = {
                    param([string] $message)
                    return "GO: '$message'"
                }
                get = {
                    param([string] $message)
                    return "GET: '$message'"
                }
                set = {
                    param([string] $message)
                    Write-Host "SET: '$message'"
                }
            }

            "write:options" = {
                options = {
                    return @{
                        "option1" = "value1"
                        "option2" = "value2"
                    }
                }
                get = {
                    param([string] $message)
                    return "GET: '$message'"
                }
                set = {
                    param([string] $value, [string] $key)
                    Write-Host "SET: '$key' to '$value'"
                }
            }
        }
    }

    It "should write message with scriptblock" {
        qbuild -map $targets "write:simple" -message "Hello, World!"

        Should -Invoke Write-Host -Exactly 1 -ParameterFilter {
            $Object -eq "SIMPLE: 'Hello, World!'"
        }
    }

    It "should write message with wrapped scriptblock" {
        qbuild -map $targets "write:wrapped" -command "exec" -message "Hello, World!"

        Should -Invoke Write-Host -Exactly 1 -ParameterFilter {
            $Object -eq "WRAPPED: 'Hello, World!'"
        }
    }

    It "should handle ordered parameters" {
        qbuild -map $targets "write:wrapped" "exec" -message "Hello, World!"

        Should -Invoke Write-Host -Exactly 1 -ParameterFilter {
            $Object -eq "WRAPPED: 'Hello, World!'"
        }
    }
}

Describe "qbuild dynamic parameters" {
    BeforeAll {
        Mock Write-Host
        
        $buildTargets = @{
            "push:short" = {
                param([switch]$NewVersion, [string]$path = $null)
                    
                Write-Host "Running push/publish workflow with args: NewVersion=$NewVersion, path=$path"
            }
            
            "push:exec"  = @{
                exec        = {
                    param([switch]$NewVersion, [string]$path = $null)
                    
                    Write-Host "Running push/publish workflow with args: NewVersion=$NewVersion, path=$path"
                }
                description = "Push/publish module (runs tests first)"
            }
        }
    }

    It "should recognize <EntryType> command parameters" -TestCases @(
        @{ EntryType = "push:short"; }
        @{ EntryType = "push:exec"; }
    ) {
        param($EntryType)
        $entry = Get-MapEntry $buildTargets $EntryType
        $scriptBlock = Get-EntryCommand $entry

        $parameters = Get-ScriptArgs $ScriptBlock
        $parameters.Keys | Should -Contain "NewVersion"
        $parameters.Keys | Should -Contain "path"
    }

    It "should handle <EntryType> command with -path parameter" -TestCases @(
        @{ EntryType = "push:short" }
        @{ EntryType = "push:exec" }
    ) {
        param($EntryType)
        
        qbuild -map $buildTargets $EntryType -path ".\src\configmap\"

        Should -Invoke Write-Host -ParameterFilter {
            $Object -eq "Running push/publish workflow with args: NewVersion=False, path=.\src\configmap\"
        }
    }

    It "should handle <EntryType> command with -NewVersion and -path parameters" -TestCases @(
        @{ EntryType = "push:short" }
        @{ EntryType = "push:exec" }
    ) {
        param($EntryType)
        
        qbuild -map $buildTargets $EntryType -NewVersion -path ".\src\configmap\"

        Should -Invoke Write-Host -ParameterFilter {
            $Object -eq "Running push/publish workflow with args: NewVersion=True, path=.\src\configmap\"
        }
    }
}

Describe "deep hierarchical execution" {
    BeforeEach {
        Mock Write-Host
        $language = Get-MapLanguage "build"
    }

    Describe "deep nesting commands" {
        BeforeAll {
            $deepMap = @{
                "level1" = @{
                    "level2" = @{
                        "level3" = @{
                            "level4" = @{
                                "level5" = {
                                    param([string]$message = "default")
                                    Write-Host "Deep level 5: $message"
                                }
                            }
                        }
                    }
                }
                "root"   = {
                    Write-Host "Root command"
                }
            }
        }

        It "should execute deep hierarchical commands" {
            qbuild -map $deepMap "level1.level2.level3.level4.level5" -message "test"
            
            Should -Invoke Write-Host -Exactly 1 -ParameterFilter {
                $Object -eq "Deep level 5: test"
            }
        }

        It "should get deep hierarchical entry" {
            $entry = Get-MapEntry $deepMap "level1.level2.level3.level4.level5"
            $entry | Should -Not -BeNullOrEmpty
            $entry | Should -BeOfType [ScriptBlock]
        }

        It "should extract parameters from deep commands" {
            $entry = Get-MapEntry $deepMap "level1.level2.level3.level4.level5"
            $parameters = Get-ScriptArgs $entry
            $parameters.Keys | Should -Contain "message"
        }
    }

    Describe "very deep nesting commands (10 levels)" {
        BeforeAll {
            $veryDeepMap = @{
                "a" = @{
                    "b" = @{
                        "c" = @{
                            "d" = @{
                                "e" = @{
                                    "f" = @{
                                        "g" = @{
                                            "h" = @{
                                                "i" = @{
                                                    "j" = {
                                                        param([string]$message = "deep", [int]$count = 1)
                                                        Write-Host "Very deep command: message=$message, count=$count"
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        It "should execute very deep hierarchical commands" {
            qbuild -map $veryDeepMap "a.b.c.d.e.f.g.h.i.j" -message "test" -count 5
            
            Should -Invoke Write-Host -Exactly 1 -ParameterFilter {
                $Object -eq "Very deep command: message=test, count=5"
            }
        }

        It "should get very deep hierarchical entry" {
            $entry = Get-MapEntry $veryDeepMap "a.b.c.d.e.f.g.h.i.j"
            $entry | Should -Not -BeNullOrEmpty
            $entry | Should -BeOfType [ScriptBlock]
        }

        It "should extract parameters from very deep commands" {
            $entry = Get-MapEntry $veryDeepMap "a.b.c.d.e.f.g.h.i.j"
            $parameters = Get-ScriptArgs $entry
            $parameters.Keys | Should -Contain "message"
            $parameters.Keys | Should -Contain "count"
        }
    }

    Describe "mixed depth hierarchical commands" {
        BeforeAll {
            $mixedMap = [ordered]@{
                "build"      = { 
                    Write-Host "build command" 
                }
                "build:exec" = [ordered]@{
                    exec        = { 
                        Write-Host "build:exec command" 
                    }
                    description = "Build command"
                }
                "db"         = [ordered]@{
                    exec           = {
                        write-host "db top-level exec"
                    }
                    "migrate"      = {
                        Write-Host "db.migrate command"
                    }
                    "migrate:exec" = [ordered]@{
                        exec        = {
                            Write-Host "db.migrate:exec command"
                        }
                        description = "Migrate command"
                    }
                    "init"         = {
                        Write-Host "db.init command"
                    }
                    "init:exec"    = [ordered]@{
                        exec = {
                            Write-Host "db.init:exec command"
                        }
                    }
                }                
            }
        }

        It "should return expected completionlist" {
            $flatList = Get-CompletionList $mixedMap -flatten:$false -reservedKeys $language.reservedKeys -leafsOnly:$true
            $flatList.Keys | Should -Be @(
                "build"
                "build:exec"
                "db.migrate"
                "db.migrate:exec"
                "db.init"
                "db.init:exec"
            )
        }

        # It "should handle mixed depth commands in tree completion" {
        # $treeList = Get-CompletionList $mixedMap -flatten:$false
        # $treeList.Keys | Should -Contain "shallow"
        # $treeList.Keys | Should -Contain "medium.sub"
        # $treeList.Keys | Should -Contain "deep.level2.level3.level4.level5"
        # }

        # It "should execute shallow command" {
        # qbuild -map $mixedMap "shallow"
        # Should -Invoke Write-Host -ParameterFilter { $Object -eq "Shallow command" }
        # }

        # It "should execute medium depth command" {
        # qbuild -map $mixedMap "medium.sub"
        # Should -Invoke Write-Host -ParameterFilter { $Object -eq "Medium depth command" }
        # }

        # It "should execute deep command" {
        # qbuild -map $mixedMap "deep.level2.level3.level4.level5"
        # Should -Invoke Write-Host -ParameterFilter { $Object -eq "Deep command" }
        # }
    }
}

Describe "custom commands" {
    BeforeAll {
        Mock Write-Host

        $mixedMap = [ordered]@{
            "db" = [ordered]@{ 
                init    = {
                    write-host "db init"
                }
                migrate = [ordered]@{
                    exec        = {
                        write-host "db migrate"
                    }
                    description = "Migrate command"
                }
            }             
        }
    }

    It "should return expected completionlist" {
        $flatList = Get-CompletionList $mixedMap -flatten:$false
        $flatList.Keys | Should -Be @(
            "db.init"
            "db.migrate"
        )
    }

    It "should return expected entries" {
        $entries = Get-MapEntries $mixedMap "db.init"
        $entries.Count | Should -Be 1
        $entries[0].Key | Should -Be "db.init"
        $entries[0].Value | Should -BeOfType [ScriptBlock]
    }

    It "should execute custom command" {
        qbuild -map $mixedMap "db.init"
        Should -Invoke Write-Host -ParameterFilter { $Object -eq "db init" }
    }

    It "should execute custom command with exec" {
        qbuild -map $mixedMap "db.migrate"
        Should -Invoke Write-Host -ParameterFilter { $Object -eq "db migrate" }
    }

    Describe "entry as submap" {
        BeforeAll {
            $entry = get-mapentry $mixedMap "db"
            $entry | Should -Not -BeNullOrEmpty
            $entry | Should -BeOfType [System.Collections.IDictionary]
        }

        It "should return expected completionlist" {
            $flatList = Get-CompletionList $entry -flatten:$false
            $flatList.Keys | Should -Be @(
                "init"
                "migrate"
            )
        }

        It "should execute custom command" {
            qbuild -map $entry "init"
            Should -Invoke Write-Host -ParameterFilter { $Object -eq "db init" }
        }
    }
}