Tests/PSNode.Tests.ps1

#Requires -Modules @{ModuleName="Pester"; ModuleVersion="5.5.0"}

using module ..\..\ObjectGraphTools

[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Object', Justification = 'False positive')]
[Diagnostics.CodeAnalysis.SuppressMessage('PSUseDeclaredVarsMoreThanAssignments', 'Node',   Justification = 'False positive')]
param()

Describe 'PSNode' {

    BeforeAll {
        Set-StrictMode -Version Latest

        $Object = @{
            String = 'Hello World'
            Array = @(
                @{ Index = 1; Name = 'One';   Comment = 'First item'  }
                @{ Index = 2; Name = 'Two';   Comment = 'Second item' }
                @{ Index = 3; Name = 'Three'; Comment = 'Third item'  }
            )
            HashTable = @{ One = 1; Two = 2; Three = 3 }
            PSCustomObject = [PSCustomObject]@{ One = 1; Two = 2; Three = 3 }
        }

        function Iterate([PSNode]$Node) { # Basic iterator
            $Node.Path
            if ($Node -is [PSCollectionNode]) {
                $Node.ChildNodes.foreach{ Iterate $_ }
            }
        }
    }

    Context 'Existence Check' {

        It 'Loaded' {
            [PSNode]::new() -is [PSNode] | Should -BeTrue
        }
    }

    Context 'ParseInput' {

        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'PSNode' {
            $Node -is [PSNode] | Should -BeTrue
        }

        it "Value" {
            $Node.Value | Should -Be $Object
        }

        it "ValueType" {
            $Node.ValueType | Should -Be 'HashTable'
            { $Node.ValueType = 'Something' } | Should -Throw '*ReadOnly*' -ExceptionType System.Management.Automation.RuntimeException
        }
    }

    Context 'Get child node' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'String' {
            $ItemNode = $Node.GetChildNode('String')
            $ItemNode -is [PSNode]     | Should -BeTrue
            $ItemNode -is [PSLeafNode] | Should -BeTrue
            $ItemNode.Value     | Should -Be $Object.String
            $ItemNode.ValueType | Should -Be $Object.String.GetType()
        }

        it 'Array' {
            $ItemNode = $Node.GetChildNode('Array')
            $ItemNode -is [PSNode]           | Should -BeTrue
            $ItemNode -is [PSCollectionNode] | Should -BeTrue
            $ItemNode -is [PSListNode]       | Should -BeTrue
            $ItemNode.ValueType | Should -Be $Object.Array.GetType()
        }

        it 'HashTable' {
            $ItemNode = $Node.GetChildNode('HashTable')
            $ItemNode -is [PSNode]           | Should -BeTrue
            $ItemNode -is [PSCollectionNode] | Should -BeTrue
            $ItemNode -is [PSMapNode]        | Should -BeTrue
            $ItemNode -is [PSDictionaryNode] | Should -BeTrue
            $ItemNode.ValueType | Should -Be $Object.HashTable.GetType()
        }

        it 'PSCustomObject' {
            $ItemNode = $Node.GetChildNode('PSCustomObject')
            $ItemNode -is [PSNode]           | Should -BeTrue
            $ItemNode -is [PSCollectionNode] | Should -BeTrue
            $ItemNode -is [PSMapNode]        | Should -BeTrue
            $ItemNode -is [PSObjectNode]     | Should -BeTrue
            $ItemNode.ValueType | Should -Be $Object.PSCustomObject.GetType()
        }
    }

    Context 'Get descendant node' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'String' {
            $ItemNode = $Node.GetNode('String')
            $ItemNode -is [PSNode]     | Should -BeTrue
            $ItemNode -is [PSLeafNode] | Should -BeTrue
            $ItemNode.Value     | Should -Be $Object.String
            $ItemNode.ValueType | Should -Be $Object.String.GetType()
        }

        it 'String' {
            $ItemNode = $Node.GetNode('.String')
            $ItemNode -is [PSNode]     | Should -BeTrue
            $ItemNode -is [PSLeafNode] | Should -BeTrue
            $ItemNode.Value     | Should -Be $Object.String
            $ItemNode.ValueType | Should -Be $Object.String.GetType()
        }

        it 'Array' {
            $ItemNode = $Node.GetNode('.Array[0].Comment')
            $ItemNode -is [PSNode]     | Should -BeTrue
            $ItemNode -is [PSLeafNode] | Should -BeTrue
            $ItemNode.Value     | Should -Be $Object.Array[0].Comment
            $ItemNode.ValueType | Should -Be $Object.Array[0].Comment.GetType()
        }
    }

    Context 'Get and set item' {
        BeforeAll {
            $Test = @{
                String = 'Hello World'
                Array = @(
                    @{ Index = 1; Name = 'One';   Comment = 'First item'  }
                    @{ Index = 2; Name = 'Two';   Comment = 'Second item' }
                    @{ Index = 3; Name = 'Three'; Comment = 'Third item'  }
                )
                HashTable = @{ One = 1; Two = 2; Three = 3 }
                PSCustomObject = [PSCustomObject]@{ One = 1; Two = 2; Three = 3 }
            }

            $Node = [PSNode]::ParseInput($Test)
        }

        it 'Read string node' {
            $ItemNode = $Node.GetChildNode('String')
            $ItemNode.Value | Should -Be 'Hello World'
        }

        it 'Write string node' {
            $ItemNode = $Node.GetChildNode('String')
            $ItemNode.Value = 'Changed'
            $ItemNode.Value                    | Should -Be 'Changed'
            $Node.GetChildNode('String').Value | Should -Be 'Changed'
        }

        it 'Array' {
            $ItemNode = $Node.GetChildNode('Array')
            $ItemNode.GetChildNode(1).GetChildNode('Comment').Value | Should -Be 'Second item'
            $ItemNode.SetItem(1, @{ Index = 2; Name = 'Two'; Comment = 'Foo Bar' })
            $ItemNode.GetChildNode(1).GetChildNode('Comment').Value | Should -Be  'Foo Bar'
        }

        it 'HashTable' {
            $ItemNode = $Node.GetChildNode('HashTable')
            $ItemNode.GetChildNode('Two').Value | Should -Be 2
            $ItemNode.SetItem('Two', 22)
            $ItemNode.GetChildNode('Two').Value | Should -Be 22
        }

        it 'PSCustomObject' {
            $ItemNode = $Node.GetChildNode('PSCustomObject')
            $ItemNode.GetChildNode('Two').Value | Should -Be 2
            $ItemNode.SetItem('Two', 22)
            $ItemNode.GetChildNode('Two').Value | Should -Be 22
        }
    }

    Context 'Get child nodes' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'Array' {
            $ItemNodes = $Node.GetChildNode('Array').ChildNodes
            $ItemNodes -is [Object[]] | Should -BeTrue
            $ItemNodes.Count          | Should -Be 3
            $ItemNodes.Value.Index    | Should -Contain 2
            $ItemNodes.Value.Name     | Should -Be 'One', 'Two', 'Three'
        }

        it 'HashTable' {
            $ItemNodes = $Node.GetChildNode('HashTable').ChildNodes
            $ItemNodes -is [Object[]] | Should -BeTrue
            $ItemNodes.Count          | Should -Be 3
            $ItemNodes.Name           | Should -Contain 'One'
            $ItemNodes.Name           | Should -Contain 'Two'
            $ItemNodes.Name           | Should -Contain 'Three'
            $ItemNodes.Value          | Should -Contain 1
            $ItemNodes.Value          | Should -Contain 2
            $ItemNodes.Value          | Should -Contain 3
        }

        it 'PSCustomObject' {
            $ItemNodes = $Node.GetChildNode('PSCustomObject').ChildNodes
            $ItemNodes -is [Object[]] | Should -BeTrue
            $ItemNodes.Count          | Should -Be 3
            $ItemNodes.Name           | Should -Be 'One', 'Two', 'Three'
            $ItemNodes.Value          | Should -Be 1, 2, 3
        }
    }

    Context 'Get descendant nodes' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'All' {
            $ItemNodes = $Node.DescendantNodes
            $ItemNodes.Count | Should -Be 22
        }
    }

    Context 'Get list child nodes' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'All' {
            $ItemNodes = $Node.ListChildNodes
            $ItemNodes.Count | Should -Be 3
        }
    }

    Context 'Get leaf nodes' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'All' {
            $ItemNodes = $Node.LeafNodes
            $ItemNodes.Count | Should -Be 16
        }
    }

    Context 'Get map child nodes' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'Array' {
            $ItemNodes = $Node.GetChildNode('Array').MapChildNodes
            $ItemNodes.Count | Should -Be 9
        }
    }

    Context 'Path' {
        BeforeAll {
            $Node = [PSNode]::ParseInput($Object)
        }

        it 'Comment path' {
            $Node.GetChildNode('Array').ChildNodes[1].GetChildNode('Comment').Path | Should -Be 'Array[1].Comment'
        }

        it 'Using variable name' {
            $Node.GetChildNode('Array').ChildNodes[1].GetChildNode('Comment').GetPathName('$MyObject') | Should -Be '$MyObject.Array[1].Comment'
        }

        it 'Array' {
            $ItemNode = $Node.GetChildNode('Array').GetChildNode(2)
            $ItemNode.Path           | Should -Be 'Array[2]'
            $ItemNode.Path.Nodes[0]  | Should -Be $ItemNode.RootNode
            $ItemNode.Path.Nodes[-1] | Should -Be $ItemNode
            $ItemNode.Path.Nodes[-2] | Should -Be $ItemNode.ParentNode
        }

        it 'HashTable' {
            $ItemNode = $Node.GetChildNode('HashTable').GetChildNode('Two')
            $ItemNode.Path           | Should -Be 'HashTable.Two'
            $ItemNode.Path.Nodes[0]  | Should -Be $ItemNode.RootNode
            $ItemNode.Path.Nodes[-1] | Should -Be $ItemNode
            $ItemNode.Path.Nodes[-2] | Should -Be $ItemNode.ParentNode
        }

        it 'PSCustomObject' {
            $ItemNode = $Node.GetChildNode('PSCustomObject').GetChildNode('Two')
            $ItemNode.Path           | Should -Be 'PSCustomObject.Two'
            $ItemNode.Path.Nodes[0]  | Should -Be $ItemNode.RootNode
            $ItemNode.Path.Nodes[-1] | Should -Be $ItemNode
            $ItemNode.Path.Nodes[-2] | Should -Be $ItemNode.ParentNode
        }

        it 'Path with space' {
            [PSNode]::ParseInput(@{ 'Hello World' = 42 }).ChildNodes[0].Path | Should -Be "'Hello World'"
        }
    }

    Context 'Recursively iterate' {

        it 'Get node paths' {
            $Actual = Iterate ([PSNode]::ParseInput($Object))
            $Expected = '', 'PSCustomObject', 'PSCustomObject.One', 'PSCustomObject.Two', 'PSCustomObject.Three', 'Array', 'Array[0]', 'Array[0].Comment', 'Array[0].Name', 'Array[0].Index', 'Array[1]', 'Array[1].Comment', 'Array[1].Name', 'Array[1].Index', 'Array[2]', 'Array[2].Comment', 'Array[2].Name', 'Array[2].Index', 'HashTable', 'HashTable.One', 'HashTable.Three', 'HashTable.Two', 'String'
            $Actual | Compare-Object $Expected | Should -BeNullOrEmpty
        }
    }


    Context '(not) exists' {
        BeforeAll {
            $Exists = @{
                String = 'Hello World'
                Array = 'One', 'Two', 'Three'
                HashTable = @{ One = 1; Two = 2; Three = 3 }
                PSCustomObject = [PSCustomObject]@{ One = 1; Two = 2; Three = 3 }
            }
            $Node = [PSNode]::ParseInput($Exists)
        }

        it '(in/out bound)' {
            { $Node.GetChildNode('NotExists') }                           | Should -Throw
            { $Node.GetChildNode('String') }                              | Should -Not -Throw
            { $Node.GetChildNode('Array') }                               | Should -Not -Throw
            { $Node.GetChildNode('Array').GetChildNode(2)}                | Should -Not -Throw
            { $Node.GetChildNode('Array').GetChildNode(3)}                | Should -Throw
            { $Node.GetChildNode('Array').GetChildNode(-3) }              | Should -Not -Throw
            { $Node.GetChildNode('Array').GetChildNode(-4) }              | Should -Throw
            { $Node.GetChildNode('HashTable').GetChildNode('One') }       | Should -Not -Throw
            { $Node.GetChildNode('HashTable').GetChildNode('one') }       | Should -Not -Throw
            { $Node.GetChildNode('HashTable').GetChildNode('Four') }      | Should -Throw
            { $Node.GetChildNode('PSCustomObject').GetChildNode('One') }  | Should -Not -Throw
            { $Node.GetChildNode('PSCustomObject').GetChildNode('one') }  | Should -Not -Throw
            { $Node.GetChildNode('PSCustomObject').GetChildNode('Four') } | Should -Throw
        }
    }

    Context 'Null or Empty' {
        BeforeAll {
            $Empties = @{
                String = ''
                Array = @()
                HashTable = @{}
                PSCustomObject = [PSCustomObject]@{}
            }
            $Node = [PSNode]::ParseInput($Empties)
        }

        it 'Empty Array' {
            $ArrayNode = $Node.GetChildNode('Array')
            $ItemNodes = $ArrayNode.ChildNodes
            $ItemNodes | Should -BeNullOrEmpty
        }

        it 'Empty HashTable' {
            $HashTableNode = $Node.GetChildNode('HashTable')
            $ItemNodes = $HashTableNode.ChildNodes
            $ItemNodes | Should -BeNullOrEmpty
        }

        it 'Empty PSCustomObject' {
            $PSCustomObjectNode = $Node.GetChildNode('PSCustomObject')
            $ItemNodes = $PSCustomObjectNode.ChildNodes
            $ItemNodes | Should -BeNullOrEmpty
        }
    }

    Context 'Warning' {
        BeforeAll {
            $Cycle = @{ Name = 'Test' }
            $Cycle.Parent = $Cycle
        }

        It 'Default Depth' {
            $Output = Iterate ([PSNode]::ParseInput($Cycle)) 3>&1
            $Output.where{$_ -is    [System.Management.Automation.WarningRecord]}.Message        | Should -BeLike  "*Parent.Parent.Parent*maximum depth*$([PSNode]::DefaultMaxDepth)*"
        }

        It '-Depth 5' {
            $Output = Iterate ([PSNode]::ParseInput($Cycle, 5)) 3>&1
            $Output.where{$_ -is    [System.Management.Automation.WarningRecord]}.Message        | Should -BeLike  '*Parent.Parent.Parent*maximum depth*5*'
        }

        It '-Depth 15' {
            $Output = Iterate ([PSNode]::ParseInput($Cycle, 15)) 3>&1
            $Output.where{$_ -is    [System.Management.Automation.WarningRecord]}.Message        | Should -BeLike  '*Parent.Parent.Parent*maximum depth*15*'
        }
    }
}