src/scriptclass.tests.ps1

# Copyright 2017, Adam Edwards
#
# 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 specific language governing permissions and
# limitations under the License.

$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path) -replace '\.Tests\.', '.'
$thismodule = join-path (split-path -parent $here) 'scriptclass.psd1'

Describe "The class definition interface" {
    BeforeAll {
        remove-module $thismodule -force 2>$null
        import-module $thismodule -force
    }

    AfterAll {
        remove-module $thismodule -force 2>$null
    }

    Context "When declaring a simple class" {
        It "succeeds with trivial parameters for the new-class cmdlet" {
            $result = add-scriptclass SimpleClass1 {}
            $result | Should BeExactly $null
        }

        It 'should capture variables defined by assignment in the class script block as data members' {
            Scriptclass ClassClass46 {
                $notypenoval = $null
                $typeimpliedbyval = 7
                $typenullvalint = [int] $null
                $typeandval = [double] 7
            }

            $newInstance = new-scriptobject ClassClass46

            $newInstance.notypenoval | Should BeExactly $null
            { ($newInstance.notypenoval).gettype() } | Should Throw

            $newInstance.typeimpliedbyval | Should BeExactly 7
            ($newInstance.typeimpliedbyval).gettype() | Should BeExactly 'int'

            $newInstance.typenullvalint | Should BeExactly 0
            ($newInstance.typenullvalint).gettype() | Should BeExactly 'int'

            $newInstance.typeandval | Should BeExactly 7
            ($newInstance.typeandval).gettype() | Should BeExactly 'double'
        }

        It "Does not throw an exception if add-scriptclass is used inside an add-scriptclass definition of a function" {
            {
                add-scriptclass ClassClassOuter72 {
                    add-scriptclass ClassClassOuter73 {}
                }
            } | Should Not Throw
        }
    }

    Context "When declaring a class with ScriptClass" {
        It "succeeds when using the ScriptClass alias" {
            $result = ScriptClass ClassClass1 {}
            $result | Should BeExactly $null
        }

        It "Does not throw an exception if ScriptClass is used inside a ScriptClass definition of a function" {
            {
                ScriptClass ClassClassOuter70 {
                    ScriptClass ClassClassOuter71 {}
                }
            } | Should Not Throw
        }

        It "allows the user to define a property on the class" {
            ScriptClass ClassClass63 {
                $mydescription = $null
            }

            $typeData = $::.ClassClass63.PSTypedata
            $typeData.members.keys -contains 'mydescription' | Should BeExactly $true
        }

        It "throws an exception if you fail to initialize a property" {
            {
                ScriptClass ClassClass50 {
                    $description
                }
            } | Should Throw
        }

        It "redefines a property with the last value if it is defined more than once" {
            $className = 'ClassClass6'
            $propertyName = 'description'
            ScriptClass $className {
                $description = 1
                $description = 2
            }

            $newInstance = new-scriptobject $className
            $newInstance.description | Should BeExactly 2
        }
    }

    Context "when creating an object from a class declared with ScriptClass" {
        ScriptClass ClassClass53 {}

        It "can create a new object using new-scriptobject with the specified type" {
            $className = 'ClassClass7'
            ScriptClass $className {}

            $newInstance = new-scriptobject $className
            $newInstance.PSTypeName | Should BeExactly $className
        }

        It "can create a new object using new-so alias for new-scriptobject with the specified type" {
            ScriptClass ClassClass66 {}

            $newInstance = new-so ClassClass66
            $newInstance.PSTypeName | Should BeExactly ClassClass66
        }

        It "has a 'scriptclass' member that has a className member equal to the class name" {
            $newInstance = new-scriptobject ClassClass53

            $newInstance.scriptclass.classname  | Should BeExactly 'ClassClass53'
        }

        It "has a 'scriptclass' member that has a className member equal to the class name" {
            $newInstance = new-scriptobject ClassClass53
            $newInstance.scriptclass | Should Not Be $null
        }

        It "has a 'scriptclass' member that has a null scriptclass member" {
            $newInstance = new-scriptobject ClassClass53
            $newInstance.scriptclass.scriptclass | Should BeExactly $null
        }

        It "has a 'scriptclass' member that has exactly six noteproperty properties and one scriptproperty property" {
            $newInstance = new-scriptobject ClassClass53
            ($newInstance.scriptclass | gm -membertype noteproperty).count | Should BeExactly 6
            ($newInstance.scriptclass | gm -membertype scriptproperty) -is [Microsoft.PowerShell.Commands.MemberDefinition] | Should BeExactly $true
        }

        It "has a 'scriptclass' member that is the same object instance as the 'scriptclass' member of a another object of the same scriptclass" {
            $newInstance = new-scriptobject ClassClass53
            $newInstance2 = new-scriptobject ClassClass53

            $newInstance.scriptclass.gethashcode() | Should BeExactly $newInstance2.scriptclass.gethashcode()
        }


        It "can create a new object that includes additional properties to the default properties" {
            $className = 'ClassClass8'
            $prop1 = 'property1'
            $prop2 = 'property2'

            ScriptClass $className {
                $property1 = $null
                $property2 = $null
            }

            $newInstance = new-scriptobject $className
            $newInstance.psobject.properties.match($prop1) | Should BeExactly $true
            $newInstance.psobject.properties.match($prop2) | Should BeExactly $true
            $newInstance.psobject.properties.match('propdoesntexist') | Should BeExactly $null
        }

        It "can create a new object that includes additional properties set to default values" {
            $className = 'ClassClass9'
            $prop1 = 'property1'
            $prop2 = 'property2'

            ScriptClass $className {
                $property1 = 1
                $property2 = 2
            }

            $newInstance = new-scriptobject $className
            $newInstance.$prop1 | Should BeExactly 1
            $newInstance.$prop2 | Should BeExactly 2
        }

        It "can create a new object that defines the type of members with strict-val" {
            $className = 'ClassClass48'
            $prop1 = 'property1'
            $prop2 = 'property2'

            ScriptClass $className {
                $property1 = strict-val [int32]
                $property2 = strict-val [Type]
            }

            $newInstance = new-scriptobject $className

            { $newInstance.$prop1 = 1 } | Should Not Throw
            { $newInstance.$prop1 = new-object object } | Should Throw
            { $newInstance.$prop2 = ([string]) } | Should Not Throw
            { $newInstance.$prop2 = '2' } | Should Throw
        }

        It "can create a new object that includes additional typed properties set to default values with strict-val" {
            $className = 'ClassClass49'
            $prop1 = 'property1'
            $prop2 = 'property2'
            $value1 = 1
            $value2 = [int32]

            ScriptClass $className {
                $property1 = strict-val [int32] $value1
                $property2 = strict-val [Type] $value2
            }

            $newInstance = new-scriptobject $className
            $newInstance.$prop1 | Should BeExactly $value1
            $newInstance.$prop2 | Should BeExactly $value2
        }

        It "can define methods on the class" {
            $className = 'ClassClass15'
            $function1 = "testFunc"
            $function2 = "testFunc2"
            $function1Result = "f1output"

            ScriptClass $className {
                function testFunc {
                    "f1output"
                }
                function testFunc2 ($arg1, $arg2) {
                    $arg1 + $arg2
                }
            }

            $newInstance = new-scriptobject $className

            ($newInstance.psobject.members | select Name).name -contains $function1 | Should BeExactly $true
            ($newInstance.psobject.members | select Name).name -contains $function2 | Should BeExactly $true
            with $newInstance $function1 | Should BeExactly $function1Result
            with $newInstance $function2 4 5 | Should BeExactly 9
        }


        It "can supply a `$this reference to methods on the class to provide access to properties defined by ScriptClass"  {
            $className = 'ClassClass16'
            $identityResult = "me"

            ScriptClass $className {
                $identity = $identityResult

                function showme {
                    $this.identity
                }
            }

            $newInstance = new-scriptobject $className
            with $newInstance showme | should BeExactly $identityResult
        }

        It "throws an exception in class definition if a typed property of the class is initialized with a value of an incompatible type" {
            $className = 'ClassClass12'
            $invalidIntegerValue = 2

            {
                ScriptClass $className {
                    $typeProperty = strict-val [Type] $invalidIntegerValue
                }
            } | Should Throw
        }

        It "cleans up PowerShell type data when one property definition throws an exception" {
            $className = 'ClassClass13'
            $invalidIntegerValue = 2

            {
                ScriptClass $className {
                    $validProperty = $null
                    $typeProperty = strict-val [Type] $invalidIntegerValue
                }
            } | Should Throw
            get-typedata $className | Should BeExactly $null
        }

        Context 'when passing a scriptclass as an argument to a function' {
            ScriptClass ClassClass62 {}
            It "should throw an exception on an attempt to pass it to a function that expects a PSCustomObject with a different PSTypeName" {
                {
                    function typedfunc([PSTypeName('somethertype')] $arg1) {}
                    typedfunc $::.ClassClass62
                } | Should Throw
            }

            It "should not throw an exception on an attempt to pass it to a function that expects a PSCustomObject with PSTypeName identical to the class name" {
                . {
                    function typedfunc([PSTypeName('ClassClass62')] $arg1) {
                        $arg1.scriptclass.classname
                    }
                    typedfunc (new-scriptobject ClassClass62)
                } | Should BeExactly 'ClassClass62'
            }
        }
    }

    Context "When new-scriptobject is used to create a new instance of a class" {
        It "calls the specified initializer function on the new object" {
            $className = 'ClassClass25'
            $initialStateValue = 3

            ScriptClass $className {
                $objectState = $null
                function __initialize {
                    $this.objectState = 3
                }
            }

            $newInstance = new-scriptobject $className

            $newInstance.objectState | Should BeExactly $initialStateValue
        }

        It "calls the specified initializer function on the new object with multiple arguments" {
            $className = 'ClassClass26'
            $initialStateValue = 9

            ScriptClass $className {
                $objectState = $null
                function __initialize($arg1, $arg2) {
                    $this.objectState = $arg1 + $arg2
                }
            }

            $newInstance = new-scriptobject $className 3 6

            $newInstance.objectState | Should BeExactly $initialStateValue

        }

        It "enables calls to the initializer to call other methods defined on the object" {
            $className = 'ClassClass27'
            $initialStateValue = 11

            ScriptClass $className {
                $objectState = $null
                function __initialize($arg1, $arg2) {
                    with $this sum $arg1 $arg2
                }

                function sum($first, $second) {
                    $this.objectState = $first + $second
                }
            }

            $newInstance = new-scriptobject $className 4 7

            $newInstance.objectState | Should BeExactly $initialStateValue
        }

        It "can use the new-so alias from within the __initialize method" {
            ScriptClass ClassClass67 { $value = 5 }
            ScriptClass ClassClass68 {
                $indirectValue = $null

                function __initialize {
                    $this.indirectValue = new-so ClassClass67
                }
            }

            $newInstance = new-so ClassClass68

            $newInstance.indirectValue.value | Should BeExactly 5
        }
    }

    Context "When a method is invoked on an object defined with ScriptClass" {
        It "can invoke other methods on the object even when the other method is defined after the calling method" {
            $className = 'ClassClass20'
            $nestedResult = 'nested'

            ScriptClass $className {
                function outer {
                    with $this inner
                }

                function inner {
                    'nested'
                }
            }

            $newInstance = new-scriptobject $className
             with $newInstance outer | Should BeExactly 'nested'
        }

        It "can invoke other methods in the object that return properties referenced from the `$this variable" {
            $className = 'ClassClass21'
            $nestedThisResult = 'nestedthis'

            ScriptClass $className {
                $objectState = 'nestedthis'
                function outer {
                    with $this inner
                }

                function inner {
                    $this.objectState
                }
            }

            $newInstance = new-scriptobject $className

            with $newInstance outer | Should BeExactly $nestedThisResult
        }

        It "can take multiple arguments and invoke other methods in the object that take multiple arguments and return results using properties referenced from the `$this variable" {
            $className = 'ClassClass22'
            $bracketResult = '[1 + (3 * 4) + 2]'

            ScriptClass $className {
                $outerBracket = '['
                $outerBracketRight = ']'
                $innerBracket = '('
                $innerBracketRight = ')'
                function sum($arg1, $arg2, $arg3, $arg4) {
                    $inner = with $this product $arg3 $arg4
                    "$($this.outerBracket)$arg1 + $inner + $($arg2)$($this.outerBracketRight)"
                }

                function product($mult1, $mult2) {
                    "$($this.innerBracket)$mult1 * $($mult2)$($this.innerBracketRight)"
                }
            }

            $newInstance = new-scriptobject $className

            with $newInstance sum 1 2 3 4 | Should BeExactly $bracketResult
        }

        It "can invoke other methods in the object using 'with' with the `$this variable" {
            $className = 'ClassClass23'

            ScriptClass $className {
                $mainValue = 7
                function outer($arg1, $arg2, $arg3) {
                    with $this inner $arg3 ($arg1 + $arg2)
                }

                function inner($first, $second) {
                    $this.mainValue + $first + $second
                }
            }

            $newInstance = new-scriptobject $className
            with $newInstance outer 4 5 6 | Should BeExactly 22
        }

        It "can invoke other methods in the object using the 'with' alias with the `$this variable and passing variable arguments using @args" {

            $className = 'ClassClass24'

            ScriptClass $className {
                $mainValue = 7
                function outer {
                    with $this inner @args
                }

                function inner($first, $second, $third) {
                    $this.mainValue + $first + $second + $third
                }
            }

            $newInstance = new-scriptobject $className
            with $newInstance outer 4 5 6 | Should BeExactly 22
        }

        It "can invoke other methods in the object using 'with' without the `$this variable by implying it" {
            $className = 'ClassClass42'

            ScriptClass $className {
                $mainValue = 7
                function outer($arg1, $arg2, $arg3) {
                    inner $arg3 ($arg1 + $arg2)
                }

                function inner($first, $second) {
                    $this.mainValue + $first + $second
                }
            }

            $newInstance = new-scriptobject $className
            with $newInstance outer 4 5 6 | Should BeExactly 22
        }

        It "can invoke other methods in the object using normal cmdlet call syntax against methods and implied `$this variable" {

            $className = 'ClassClass41'

            ScriptClass $className {
                $mainValue = 7
                function outer {
                    inner @args
                }

                function inner($first, $second, $third) {
                    $this.mainValue + $first + $second + $third
                }
            }

            $newInstance = new-scriptobject $className
            with $newInstance outer 4 5 6 | Should BeExactly 22
        }

        It "can use the -do parameter to specify a method or scriptblock" {

            $className = 'ClassClass44'

            ScriptClass $className {
                $mainValue = 7
                function outer {
                    inner @args
                }

                function inner($first, $second, $third) {
                    $this.mainValue + $first + $second + $third
                }
            }

            $newInstance = new-scriptobject $className
            with $newInstance -do outer 4 5 6 | Should BeExactly 22
            with $newInstance -do { outer 3 2 1 } | Should BeExactly 13
        }

    }

    Context "When a class is composed with another class" {
        ScriptClass Inner {
            $state = 0
            function __initialize($initState) {
                $this.state = $initState
            }

            function Eval($base, $exponent) {
                [Math]::Pow($base, $exponent) + $this.state
            }
        }

        ScriptClass Outer {
            $evaluator = $null
            function __initialize($initialOffset) {
                $this.evaluator = new-scriptobject Inner $initialOffset
            }

            function getvalue($base, $exp) {
                with $this.evaluator Eval $base $exp
            }
        }

        It "Should have instances that can call methods of one class from another class" {
            $newInstance = new-scriptobject Outer 5
            with $newInstance getvalue 2 3 | Should BeExactly 13
        }
    }

    Context "When redefining a class" {
        It "doesn't throw an exception when the class is defined the same way twice" {
            ScriptClass SimpleClass3 {}
            { ScriptClass SimpleClass3 {} } | Should Not Throw
        }

        It "redefines an existing class if it already exists" {
            ScriptClass ClassClass51 {
                $prop1 = 1
                $prop2 = 2
                $prop3 = strict-val [int] 3
                function method1 { $this.prop1 }
                function method2 { $this.prop2 }
                function method3 { $this.prop3 }
            }

            ScriptClass ClassClass51 {
                $prop2 = 21
                $prop3 = strict-val [string] '31'
                $prop4 = 4
                $prop5 = $null
                function method2 { $this.prop4 }
                function method4 { $this.prop3 }
                function method5 { $this.prop5 }
                function __initialize { $this.prop5 = 5 }
            }

            $newInstance = new-scriptobject ClassClass51

            { $newInstance.prop1 | out-null } | Should Throw
            $newInstance.prop2 | Should BeExactly 21
            $newInstance.prop3 | Should BeExactly '31'
            $newInstance.prop4 | Should BeExactly 4
            $newInstance.prop5 | Should BeExactly 5

            { $newInstance |=> method1 | out-null } | Should Throw
            $newInstance |=> method2 | Should BeExactly 4
            { $newInstance |=> method3 | out-null } | Should Throw
            $newInstance |=> method4 | Should BeExactly '31'
            $newInstance |=> method5 | Should BeExactly 5
        }
    }
}

Describe "The get-class cmdlet" {
    BeforeAll {
        remove-module $thismodule -force 2>$null
        import-module $thismodule -force
    }

    AfterAll {
        remove-module $thismodule -force 2>$null
    }

    ScriptClass ClassClass59 {
    }

    Context "When getting information about a class" {
        It "should return an object equal to the class's scriptclass property" {
            $newInstance = new-scriptobject ClassClass59
            (get-class ClassClass59) | Should BeExactly $newInstance.scriptclass
        }

        It "should return an object with a null scriptclass" {
            (get-class ClassClass59).scriptclass | Should BeExactly $null
        }

        It "should throw an exception if the class does not exist" {
            { get-class idontexist } | Should Throw
        }
    }
}

Describe 'The $:: collection' {
    BeforeAll {
        remove-module $thismodule -force 2>$null
        import-module $thismodule -force
    }

    AfterAll {
        remove-module $thismodule -force 2>$null
    }

    ScriptClass ClassClass60 {}
    ScriptClass ClassClass60a {}
    ScriptClass ClassClass60b {}

    Context 'When accessing the $:: collection' {
        It "should return a class object when the name of the class is specified on it after '.'" {
            $result1 = $::.ClassClass60
            $result1 | Should BeExactly (get-class ClassClass60)
            $result2 = $::.ClassClass60a
            $result2 | Should BeExactly (get-class ClassClass60a)
            $result3 = $::.ClassClass60b
            $result3 | Should BeExactly (get-class ClassClass60b)
        }

        It "should throw an exception when a non-existent class name is specified after '.'" {
            { $::.idontexist } | Should Throw
        }

        It "should return a class object that has a pstypedata property" {
            $::.ClassClass60.pstypedata | Should Not Be $null
        }

        It "should return a class object that has a ClassScriptBlock member of its scriptclass by default" {
            add-scriptclass ClassClass62 { 62 }
            $classType = $::.ClassClass62.pstypedata
            $invokeResult = invoke-command -scriptblock $classType.members.ScriptClass.value.ClassScriptBlock
            $invokeResult | Should BeExactly 62
        }

        It "should return a class object that has a $null scriptclass property" {
            $::.ClassClass60.scriptclass | Should Be $null
        }

        It "should throw an exception on an attempt to access a nonexistent property of the class object " {
            { $::.ClassClass60.idontexist } | Should Throw
        }
    }
}

Describe "'with' function for object-based command context" {
    BeforeAll {
        remove-module $thismodule -force 2>$null
        import-module $thismodule -force
    }

    AfterAll {
        remove-module $thismodule -force 2>$null
    }

    Context "When invoking an object's method through with" {
        $className = 'ClassClass32'

        ScriptClass $className {
            $mainValue = 7
            function outer {
                with $this inner @args
            }

            function inner($first, $second, $third) {
                $this.mainValue + $first + $second + $third
            }

            function singlearg($first) {
                $this.mainValue + $first
            }
        }

        $newInstance = new-scriptobject $className

        It "throws an exception if a null object is specified" {
            { with $null inner } | Should Throw
        }

        It "throws an exception if a non-string or non-scriptblock type is passed as the action" {
           { with $newInstance 3.0 } | Should Throw
        }

        It "throws an exception the context object is of a type cannot be cast as a PSCustomObject" {
            { with 3 'tostring' } | Should Throw
        }

        It "successfully executes a method that takes no arguments" {
            with $newInstance outer | Should BeExactly 7
        }

        It "successfully executes a method that takes 1 argument" {
            with $newInstance singlearg 4 | Should BeExactly 11
        }

        It "successfully executes a method that takes more than one argument" {
            with $newInstance outer 5 6 7 | Should BeExactly 25
        }

        It "throws an exception if a non-existent method for the object is specified" {
            { with $newInstance idontexist } | Should Throw
        }

        It "successfully executes a block that takes no arguments" {
            with $newInstance {$this.mainValue} | Should BeExactly 7
        }

        It "successfully executes a block that takes at least one argument" {
            with $newInstance {$this.mainValue + $args[0]} 2 | Should BeExactly 9
        }

        It "successfully executes a block that uses a method like a function" {
            with $newInstance { outer 10 20 30 } | Should BeExactly 67
        }

        It "successfully executes a block that uses a method like a function and passes arguments to it through @args" {
            with $newInstance { outer @args } 10 20 40 | Should BeExactly 77
        }
    }

    Context "When invoking an pscustomobject's method through with" {
        $newInstance = [PSCustomObject]@{first=1;second=2;third=3}
        $summethod = @{name='sum';memberType='ScriptMethod';value={$this.first + $this.second + $this.third}}
        $addmethod = @{name='add';memberType='ScriptMethod';value={param($firstarg, $secondarg) $firstarg + $secondarg}}
        $addtomethod = @{name='addto';memberType='ScriptMethod';value={param($firstarg) $this.sum() + $firstarg}}

        $newInstance | add-member @summethod
        $newInstance | add-member @addmethod
        $newInstance | add-member @addtomethod

        It "successfully executes a method that takes no arguments" {
            with $newInstance { sum } | Should BeExactly 6
        }

        It "successfully executes a method that takes 1 argument" {
            with $newInstance { addto 10 } Should Be Exactly 16
        }

        It "successfully executes a method that takes more than one argument" {
            with $newInstance { add 5 7 } Should Be Exactly 12
        }

        It "throws an exception if a non-existent method for the object is specified" {
            { with $newInstance { run } } | Should Throw
        }

        It "successfully executes a block that takes no arguments" {
           with $newInstance { $this.first } | Should BeExactly 1
        }

        It "successfully executes a block that takes at least one argument" {
            with $newInstance { add @args } 4 6 | Should BeExactly 10
            with $newInstance { addto @args } 7 | Should BeExactly 13
        }
    }
}

Describe 'The => invocation function' {
    BeforeAll {
        remove-module $thismodule -force 2>$null
        import-module $thismodule -force
    }

    AfterAll {
        remove-module $thismodule -force 2>$null
    }

    Context "When a method is invoked through the => function" {
        $initialValue = 10
        ScriptClass ClassClass43 {
            $sum = $initialValue
            function add($first, $second) {
                $first + $second
            }

            function addto($firstarg) {
                $this.sum += $firstarg
                current
            }

            function current() {
                $this.sum
            }

            static {
                function staticmethod {
                }
            }
        }

        $newInstance = new-scriptobject ClassClass43
        $newInstance2 = new-scriptobject ClassClass43
        $newInstance3 = new-scriptobject ClassClass43

        It "Should execute a method with no arguments" {
            $newInstance | => current | Should BeExactly $initialValue
        }

        It "Should execute a method with two arguments" {
            $newInstance | => add 1 3 | Should BeExactly 4
        }

        It "Should execute a method that takes an argument and incorporates object state" {
            $newInstance | => addto 2 | Should BeExactly 12
        }

        It "Should execute the same method on two different objects" {
            $newInstance2 |=> addto 3 | out-null
            $results = $newInstance2, $newInstance3 |=> addto 4

            $results[0] | Should BeExactly 17
            $results[1] | Should BeExactly 14
        }

        It "Should throw an exception if nothing is piped to it" {
            { => somemethod current } | Should Throw
        }

        It "Should throw an exception if no method is specified" {
            { $newInstance | => } | Should Throw
        }

        It "Should throw an exception if a non-existent method is specified" {
           {$newInstance |=> nonexistent} | Should Throw
        }

        It "Should throw an exception if a static method is specified" {
            {$newInstance |=> staticmethod} | Should Throw
        }

        It "Should invoke static methods when used on an instance's scriptclass property" {
            $newInstance.scriptclass |=> staticmethod 25 31 Should BeExactly 56
        }
    }
}

Describe 'Static functions' {
    BeforeAll {
        remove-module $thismodule -force 2>$null
        import-module $thismodule -force
    }

    AfterAll {
        remove-module $thismodule -force 2>$null
    }

    ScriptClass ClassClass52 {
        static {
            function staticmethod($arg1, $arg2) {
                $arg1 + $arg2
            }

            function staticmethod2($arg1, $arg2) {
                $this |=> staticmethod $arg1 $arg2
            }
        }

        function instancemethod {}
    }

    Context "When a static method is invoked through ::>" {
        It 'Should accept the name of the class as a string as the class on which to call the method' {
            'ClassClass52' |::> staticmethod 8 5 | Should BeExactly 13
        }

        It "should accept the scriptclass property of an instance as a way to invoke the static method" {
            $newInstance = new-scriptobject ClassClass52
            with $newInstance.scriptclass staticmethod 20 50 Should BeExactly 70
        }

        It "should be accessible from within the class initializer" {
            ScriptClass ClassClass74 {
                $mainValue = $null
                static {
                    function InitialVal {
                        7
                    }
                }
                function __initialize {
                    $this.mainValue = ('ClassClass74' |::> InitialVal)
                }
            }

            $newInstance = new-so ClassClass74

            $newInstance.mainValue | Should BeExactly 7
        }

        It "has a `$this variable available to static methods that enables access to other static members in the class" {
            $::.ClassClass52 |=> staticmethod2 13 3 | Should BeExactly 16
        }

        It "should throw an exception if the type piped to ::> is not a string" {
            { $::.ClassClass52 |::> instancemethod } | Should Throw
        }

        It "should throw an exception if the class piped to ::> does not exist" {
            { 'idontexist' |::> instancemethod } | Should Throw
        }

        It "should throw an exception if the method passed to ::> does not exist" {
            { 'ClassClass52' |::> idontexist } | Should Throw
        }


        It "Should throw an exception if the operator is used to call an instance method" {
            { 'ClassClass52' |::> instancemethod } | Should Throw
        }
    }

    Context "When a static method is invoked through invoke-method or with or =>" {
        It 'Should accept the result of get-class as the class on which to call the method for invoke-methodwithcontext' {
            invoke-method (get-class ClassClass52) staticmethod 2 3 | Should BeExactly 5
        }

        It 'Should accept the result of get-class as the class on which to call the method for "with"' {
            with (get-class ClassClass52) staticmethod 2 3 | Should BeExactly 5
        }

        It 'Should allow invocation of the static method by supplying "with" with a block' {
            with (get-class ClassClass52) { staticmethod 10 40 } Should BeExactly 50
        }

        It 'Should allow invocation of the static method by supplying scriptclass method as the object' {
            $newInstance = new-scriptobject ClassClass52
            with $newInstance.scriptclass staticmethod 20 50 Should BeExactly 70
        }

        It "Should accept the `$:: variable's property named by the class as the class on which to call the method using =>" {
            $::.ClassClass52 |=> staticmethod 10 4 | Should BeExactly 14
        }

        It 'Should accept the result of get-class as the class on which to call the method using =>' {
            (get-class ClassClass52) |=> staticmethod 2 3 | Should BeExactly 5
        }
    }

    Context "When defining static methods" {
        It "Should allow an instance method and a static method to have the same name" {
            {
                ScriptClass ClassClass55 {
                    function bothtypes {}
                    static { function bothtypes {} }
                }
            } | Should Not Throw
        }

        Context "when static and instance methods have the same name and the instance method is defined first" {
            ScriptClass ClassClass56 {
                function bothtypes {
                    7
                }

                static {
                    function bothtypes {
                        5
                    }
                }
            }

            It "Should invoke the static method when the ::> method is used" {
                'ClassClass56' |::> bothtypes | Should BeExactly 5
            }

            It "Should invoke the instance method when the => function is used" {
                $newInstance = new-scriptobject ClassClass56
                $newInstance |=> bothtypes | Should BeExactly 7
            }

            It "Should invoke the static method when the => function is supplied with an instance's scriptclass property" {
                $newInstance = new-scriptobject ClassClass56
                $newInstance.scriptclass |=> bothtypes | Should BeExactly 5
            }
        }

        Context "when static and instance methods have the same name and the static method is defined first" {
            ScriptClass ClassClass57 {
                static {
                    function bothtypes {
                        5
                    }
                }

                function bothtypes {
                    7
                }
            }

            It "Should invoke the static method when the ::> method is used" {
                'ClassClass57' |::> bothtypes | Should BeExactly 5
            }

            It "Should invoke the instance method when the => function is used" {
                $newInstance = new-scriptobject ClassClass57
                $newInstance |=> bothtypes | Should BeExactly 7
            }

            It "Should invoke the static method when the => function is supplied with an instance's scriptclass property" {
                $newInstance = new-scriptobject ClassClass57
                $newInstance.scriptclass |=> bothtypes | Should BeExactly 5
            }
        }

        Context "when the static method is defined twice" {
            ScriptClass ClassClass58 {
                static {
                    function bothtypes {
                        5
                    }
                    function bothtypes {
                        6
                    }
                }

                function bothtypes {
                    7
                }
            }

            It "Should invoke the last static method defined when the ::> method is used" {
                'ClassClass58' |::> bothtypes | Should BeExactly 6
            }

            It "Should invoke the instance method when the => function is used" {
                $newInstance = new-scriptobject ClassClass58
                $newInstance |=> bothtypes | Should BeExactly 7
            }
        }

        It "Does not allow the use of 'static' within a static block" {
            {
                ScriptClass ClassClass80 {
                    static {
                        static {
                            $thisshouldnotwork = $null
                        }
                    }
                }
            } | Should Throw
        }
    }
}

Describe 'Static member variables' {
    BeforeAll {
        remove-module $thismodule -force 2>$null
        import-module $thismodule -force
    }

    AfterAll {
        remove-module $thismodule -force 2>$null
    }

    Context "When declaring a class with static member variables" {
        ScriptClass ClassClass75 {
            static {
                $var1 = $null
                $var2 = 7

                function getvar {
                    $this.var2
                }
            }
        }

        It "should not have variables accessible through the ::> function for the class" {
            {
                'ClassClass75' |::> var2 | Should BeExactly 7
            } | Should Throw
        }

        It 'should have variables accessible through the $:: member for the class' {
            $::.ClassClass75.var1 | Should BeExactly $null
            $::.ClassClass75.var2 | Should BeExactly 7
        }

        It 'should have variables accessible through the scriptclass member of an instance' {
            $newInstance = new-so ClassClass75
            $newInstance.scriptclass.var1 | Should BeExactly $null
            $newInstance.scriptclass.var2 | Should BeExactly 7
        }

        It 'should have static member variables available to static methods through a $this variable' {
            $::.ClassClass75 |=> getvar | Should BeExactly 7
        }

        It "should throw an exception if there is an attempt to access variables through the ::> function for the class" {
            { 'ClassClass75' |::> var2 | out-null } | Should Throw
        }

        It "should throw an exception if a static variable that was not defined is passed to the ::> function" {
            { 'ClassClass75' |::> var3 | out-null } | Should Throw
        }

        It 'should throw an exception if a static variable that was not defined is accessed as a member of $::' {
            { $::.ClassClass75.var3 | out-null } | Should Throw
        }

        It 'should throw an exception if a static variable that was not defined is accessed as a member of an instance scriptclass member' {
            $newInstance = new-so ClassClass75
            { $newInstance.var3 | out-null } | Should Throw
        }

        It 'should update the value of the variable when it is assigned by accessing the class member of $::' {
            ScriptClass ClassClass76 {
                static {
                    $var1 = 4
                }
            }
            $::.ClassClass75.var1 = 10
            $::.ClassClass75.var1 | Should BeExactly 10
        }

        It 'should be accessible for read and write through static and instance methods' {
            ScriptClass ClassClass77 {
                static {
                    $instances = 0
                    function InstanceCount {
                        $this.instances
                    }
                }

                function __initialize {
                    $this.scriptclass.instances++
                }
            }

            $newInstance = new-so ClassClass77
            $::.ClassClass77.instances | Should BeExactly 1
            $::.ClassClass77 |=> InstanceCount | Should BeExactly 1
            $secondInstance = new-so ClassClass77

            $::.ClassClass77 |=> InstanceCount | Should BeExactly 2
            $::.ClassClass77 |=> InstanceCount | Should BeExactly $::.ClassClass77.instances
        }
    }

    Context "When static variables are defined with the same name as a non-static variable" {
        ScriptClass ClassClass78 {
            $bothtypes = 7
            static { $bothtypes = 5 }
        }

        $newInstance = new-so ClassClass78

        It 'should allow static and non-static variables of the same name to be defined' {
            $newInstance.bothtypes | Should BeExactly 7
            $newInstance.scriptclass.bothtypes | Should BeExactly 5
        }
    }

    It "Does not allow the use of 'static' within a static block" {
        {
            ScriptClass ClassClass80 {
                static {
                    static {
                        $thisshouldnotwork = $null
                    }
                }
            }
        } | Should Throw
    }
}

Describe 'Typed static member variables' {
    BeforeAll {
        remove-module $thismodule -force 2>$null
        import-module $thismodule -force
    }

    AfterAll {
        remove-module $thismodule -force 2>$null
    }

    Context 'When declaring typed static members with strict-val' {
        ScriptClass ClassClass79 {
            static {
                $stuff = strict-val [object]
                $stuffint = strict-val [int32]
                $stufftype = strict-val [Type]
                $stuffintval = strict-val [int] 1
                $stufftypeval = strict-val [Type] ([int])
            }
        }

        It 'should allow the typed static member of type [object] with no initializer to evaluate as $null' {
            $::.ClassClass79.stuff | Should BeExactly $null
        }

        It 'should allow the typed static member of type [object] with no initializer to be assigned a value' {
            $::.ClassClass79.stuff = 512
            $::.ClassClass79.stuff | Should BeExactly 512
        }

        It "should enforce type mismatch errors when defining" {
            { $::.ClassClass79.stuffint = new-object object } | Should Throw
            { $::.ClassClass79.stufftype = ([string]) } | Should Not Throw
            { $::.ClassClass79.stufftype = '2' } | Should Throw
        }

        It "can create a new object that includes additional typed properties set to default values with strict-val" {
            $::.ClassClass79.stuffintval | Should BeExactly 1
            $::.ClassClass79.stufftypeval | Should BeExactly ([int])
        }
    }
}

Describe 'The test-scriptobject cmdlet' {
    BeforeAll {
        remove-module $thismodule -force 2>$null
        import-module $thismodule -force
    }

    AfterAll {
        remove-module $thismodule -force 2>$null
    }

    ScriptClass ClassClass64 {}
    ScriptClass ClassClass65 {}
    $newInstance = new-scriptobject ClassClass64
    It 'Should return $true if only a scriptclass object instance is specified by position' {
        test-scriptobject $newInstance | Should BeExactly $true
    }

    It 'Should return $true if a scriptclass object instance is specified through the pipeline' {
        $newInstance | test-scriptobject | Should BeExactly $true
    }

    It 'Should return $true if a scriptclass object is specified with its script class type name' {
        test-scriptobject $newInstance ClassClass64 | Should BeExactly $true
    }

    It 'Should return $true if a scriptclass object is specified with its scriptclass class object' {
        test-scriptobject $newInstance $::.ClassClass64 | Should BeExactly $true
    }

    It 'Should return $false if a scriptclass object is specified with a valid scriptclass class name of a different scriptclass than the instance' {
        test-scriptobject $newInstance 'ClassClass65' | Should BeExactly $false
    }

    It 'Should return $false if a scriptclass object is specified with a valid scriptclass class object of a different scriptclass than the instance' {
        test-scriptobject $newInstance $::.ClassClass65 | Should BeExactly $false
    }

    It 'Should return $false if only a non-scriptclass object is specified' {
        test-scriptobject [Type] | Should BeExactly $false
        test-scriptobject 3 | Should BeExactly $false
    }

    It 'Should return $false if non-scriptclass object is specified with a scriptclass type name' {
        test-scriptobject ([Type]) ClassClass64 | Should BeExactly $false
        test-scriptobject 3 ClassClass64 | Should BeExactly $false
    }

    It 'Should throw an exception if the scriptclass parameter is a string that is not the name of defined scriptclass' {
        { test-scriptobject $newInstance 'idontexist' } | Should Throw
    }

    It 'Should throw an exception if the scriptclass parameter is not a PSCustomObject' {
        { test-scriptobject $newInstance 3 | out-null } | Should Throw
    }

    It 'Should throw an exception if the scriptclass parameter is not a PSCustomObject created with new-scriptobject with a PSTypeName that matches the class' {
        $custom = [PSCustomObject]@{field1=1;field2=2}
        $typedcustom = [PSCustomObject]@{field1=1;field2=2;PSTypeName='notascriptclass'}
        { test-scriptobject $newInstance $custom | out-null } | Should Throw
        { test-scriptobject $newInstance $typedcustom | out-null } | Should Throw
    }
}

Describe "The const cmdlet" {
    BeforeAll {
        remove-module $thismodule -force 2>$null
        import-module $thismodule -force
    }

    AfterAll {
        remove-module $thismodule -force 2>$null
    }

    function clean-variable($name) {
        $existing = $true

        @('Script', 'Local', 1) | foreach {
            $existing = try {
                get-variable -name $name -scope $_ 2> $null
            } catch {
                $null
            }

            if ($existing -ne $null) {
                $existing | remove-variable -scope $_ -force
            } else {
                break
            }
        }
    }

    function variable-exists($name) {
        (get-variable -name $name 2> $null) -ne $null
    }

    BeforeEach {
        clean-variable testvar
        variable-exists testvar | Should Be False
    }

    AfterEach {
        clean-variable testvar
        variable-exists testvar | Should Be False
    }

    Context "when defining constants" {
        ScriptClass ConstTest0 {
            const testConst 159
            const strictConst (strict-val [double] 11)
            function writeConstant($value) {
                $this.testConst = $value
            }
        }
        It "creates a read-only variable with the specified value" {
            $newInstance = new-so ConstTest0
            $newInstance.testConst | Should BeExactly 159
        }

        It "throws an exception if an assignment is made to the constant even if the value is the same as the existing value" {
            { ScriptClass ConstTest1 { const testvar 159; $testvar = 157 } } | Should Throw "Cannot Overwrite"
            { ScriptClass ConstTest2 { const testvar 159; $testvar = 159 } } | Should Throw "Cannot overwrite"
        }

        It "throws an exception if an assignment is made to the constant by consumers of the object" {
            $newInstance = new-so ConstTest0
            { $newInstance.testConst = $newInstance.testConst } | Should Throw "Exception setting"
            { $newInstance.testConst = ( $newInstance.testConst - 1) } | Should Throw "Exception setting"
        }

        It "throws an exception if an assignment is made to the constant by methods of the object" {
            $newInstance = new-so ConstTest0
            { $newInstance |=> writeConstant $newInstance.testConst } | Should Throw "Exception setting"
            { $newInstance |=> writeConstant ( $newInstance.testConst - 1 ) } | Should Throw "Exception setting"
        }

        It "throws an exception if an attempt is made to define it with a different value" {
            { ScriptClass ConstTest3 { const testvar 156; const testvar 157} } | Should Throw "Attempt to redefine"
        }

        It "does not throw an exception if const is used to define the value more than once with the same value" {
            { ScriptClass ConstTest4 { const testvar 159; const testvar 159} } | Should Not Throw
        }

        It "does not conflict with a variable with the same name defined at script scope" {
            ScriptClass ConstTest5 { const testvar 159; function getval { $this.testvar } }
            new-variable testvar -scope script -value 157
            $newInstance = new-so ConstTest5
            $newInstance.testvar | Should BeExactly 159
            $newInstance |=> getval | Should BeExactly 159
            $testvar | Should BeExactly 157
            $script:testvar | Should BeExactly 157
        }

        It "does not conflict with a variable with the same name defined at local scope" {
            ScriptClass ConstTest6 { const testvar 159; function getval { $testvar = 5; $testvar } }
            $newInstance = new-so ConstTest6
            $newInstance.testvar | Should BeExactly 159
            $newInstance |=> getval | Should BeExactly 5
        }

        It "defines strictly typed constants when used with 'strict-val'" {
            $newInstance = new-so ConstTest0
            $newInstance.strictConst.gettype() | Should BeExactly 'double'
            ([int] $newInstance.strictConst) | Should BeExactly 11
        }

    }
}