Viscalyx.Assert.psm1

#Region '.\prefix.ps1' -1

$script:dscResourceCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules/DscResource.Common'
Import-Module -Name $script:dscResourceCommonModulePath

$script:viscalyxCommonModulePath = Join-Path -Path $PSScriptRoot -ChildPath 'Modules/Viscalyx.Common'
Import-Module -Name $script:viscalyxCommonModulePath

$script:localizedData = Get-LocalizedData -DefaultUICulture 'en-US'
#EndRegion '.\prefix.ps1' 8
#Region '.\Private\Get-TypeName.ps1' -1

<#
    .SYNOPSIS
        Gets the full type name of a value.
 
    .DESCRIPTION
        The `Get-TypeName` command returns the full type name of a value. If the
        value is `$null`, it returns the string 'null' instead of attempting to
        call GetType() on a null reference.
 
    .PARAMETER Value
        The value to get the type name for.
 
    .INPUTS
        None
 
        This command does not accept pipeline input.
 
    .OUTPUTS
        System.String
 
        Returns the full type name as a string, or 'null' if the value is null.
 
    .EXAMPLE
        Get-TypeName -Value 'Hello'
 
        Returns 'System.String'.
 
    .EXAMPLE
        Get-TypeName -Value $null
 
        Returns 'null'.
 
    .EXAMPLE
        Get-TypeName -Value 123
 
        Returns 'System.Int32'.
#>

function Get-TypeName
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [System.Object]
        $Value
    )

    if ($null -eq $Value)
    {
        return 'null'
    }

    return $Value.GetType().FullName
}
#EndRegion '.\Private\Get-TypeName.ps1' 57
#Region '.\Private\New-AssertionError.ps1' -1

<#
    .SYNOPSIS
        Creates a Pester assertion error record.
 
    .DESCRIPTION
        The `New-AssertionError` function creates a Pester-formatted error record
        with optional 'because' reasoning. This standardizes error creation across
        assertion commands.
 
    .PARAMETER Message
        The error message to include in the assertion error.
 
    .PARAMETER Because
        An optional reason or explanation for the assertion failure.
 
    .PARAMETER InvocationInfo
        The invocation information from the calling command, used to populate
        the error record with script name, line number, and line content.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.Management.Automation.ErrorRecord
 
        Returns a Pester-formatted error record.
 
    .EXAMPLE
        New-AssertionError -Message "Property not found" -Because "it is required" -InvocationInfo $MyInvocation
 
        Creates an assertion error with a because clause.
#>

function New-AssertionError
{
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '', Justification = 'This function does not change system state, it only creates an error record object.')]
    [CmdletBinding()]
    [OutputType([System.Management.Automation.ErrorRecord])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Message,

        [Parameter()]
        [System.String]
        $Because,

        [Parameter(Mandatory = $true)]
        [System.Management.Automation.InvocationInfo]
        $InvocationInfo
    )

    if ($Because)
    {
        # Insert 'because <reason>' before ', but' to follow Pester's pattern:
        # "Expected <value>, because <reason>, but got <actual>"
        if ($Message -match ',\s+but\s+')
        {
            $Message = $Message -replace ',\s+but\s+', (", {0} $Because, but " -f $script:localizedData.Common_WordBecause)
        }
        else
        {
            # Fallback: append at the end if no ', but' pattern found
            $Message += " {0} $Because" -f $script:localizedData.Common_WordBecause
        }
    }

    $errorRecord = [Pester.Factory]::CreateShouldErrorRecord(
        $Message,
        $InvocationInfo.ScriptName,
        $InvocationInfo.ScriptLineNumber,
        $InvocationInfo.Line.TrimEnd([System.Environment]::NewLine),
        $true
    )

    return $errorRecord
}
#EndRegion '.\Private\New-AssertionError.ps1' 78
#Region '.\Private\Test-ObjectHasMethod.ps1' -1

<#
    .SYNOPSIS
        Tests whether an object has a specified method.
 
    .DESCRIPTION
        The `Test-ObjectHasMethod` function checks if the given object contains
        a method with the specified name. This function supports various object
        types including PSCustomObjects and .NET objects.
 
    .PARAMETER InputObject
        The object to test for the method.
 
    .PARAMETER MethodName
        The name of the method to check for.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.Boolean
 
        Returns $true if the method exists, $false otherwise.
 
    .EXAMPLE
        Test-ObjectHasMethod -InputObject $myObject -MethodName 'ToString'
 
        Returns $true if $myObject has a method named 'ToString', otherwise $false.
#>

function Test-ObjectHasMethod
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Object]
        $InputObject,

        [Parameter(Mandatory = $true)]
        [System.String]
        $MethodName
    )

    $hasMethod = $false

    try
    {
        # Use Get-Member to check for method existence
        $member = $InputObject | Get-Member -Name $MethodName -MemberType Method, ScriptMethod -ErrorAction SilentlyContinue
        if ($null -ne $member)
        {
            $hasMethod = $true
        }
        else
        {
            # For dynamic objects and PSCustomObject, also check PSObject.Methods
            $methodMember = $InputObject.PSObject.Methods[$MethodName]
            if ($null -ne $methodMember)
            {
                $hasMethod = $true
            }
            else
            {
                # Final check: try to get the method directly for edge cases
                try
                {
                    $methodInfo = $InputObject.GetType().GetMethod($MethodName)
                    if ($null -ne $methodInfo)
                    {
                        $hasMethod = $true
                    }
                }
                catch
                {
                    # If reflection fails, the method doesn't exist
                    $hasMethod = $false
                }
            }
        }
    }
    catch
    {
        $hasMethod = $false
    }

    return $hasMethod
}
#EndRegion '.\Private\Test-ObjectHasMethod.ps1' 88
#Region '.\Private\Test-ObjectHasProperty.ps1' -1

<#
    .SYNOPSIS
        Tests whether an object has a specified property.
 
    .DESCRIPTION
        The `Test-ObjectHasProperty` function checks if the given object contains
        a property with the specified name. This function supports various object
        types including hashtables, PSCustomObjects, and .NET objects.
 
    .PARAMETER InputObject
        The object to test for the property.
 
    .PARAMETER PropertyName
        The name of the property to check for.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.Boolean
 
        Returns $true if the property exists, $false otherwise.
 
    .EXAMPLE
        Test-ObjectHasProperty -InputObject $myObject -PropertyName 'Name'
 
        Returns $true if $myObject has a property named 'Name', otherwise $false.
#>

function Test-ObjectHasProperty
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Object]
        $InputObject,

        [Parameter(Mandatory = $true)]
        [System.String]
        $PropertyName
    )

    $hasProperty = $false

    try
    {
        # For hashtables, check if the key exists
        if ($InputObject -is [System.Collections.IDictionary])
        {
            $hasProperty = $InputObject.ContainsKey($PropertyName)
        }
        # For PSCustomObject and other objects, try PSObject.Properties first
        elseif ($null -ne $InputObject.PSObject.Properties[$PropertyName])
        {
            $hasProperty = $true
        }
        # Use Get-Member as fallback for .NET objects
        else
        {
            $member = $InputObject | Get-Member -Name $PropertyName -MemberType Property, NoteProperty, ScriptProperty -ErrorAction SilentlyContinue
            if ($null -ne $member)
            {
                $hasProperty = $true
            }
            else
            {
                # Final check with direct property access for edge cases
                try
                {
                    $null = $InputObject.$PropertyName
                    # If we got here without exception, the property exists
                    # But we need to make sure it's not just returning $null for non-existent properties
                    $actualMember = $InputObject.PSObject.Members | Where-Object { $_.Name -eq $PropertyName }
                    $hasProperty = $null -ne $actualMember
                }
                catch
                {
                    $hasProperty = $false
                }
            }
        }
    }
    catch
    {
        $hasProperty = $false
    }

    return $hasProperty
}
#EndRegion '.\Private\Test-ObjectHasProperty.ps1' 91
#Region '.\Private\Test-ObjectType.ps1' -1

<#
    .SYNOPSIS
        Tests whether two values have the same type.
 
    .DESCRIPTION
        The `Test-ObjectType` function checks if two values are of the same type.
        This is used for strict type checking in assertion commands to ensure type
        compatibility before comparing values.
 
    .PARAMETER ActualValue
        The actual value to check the type of.
 
    .PARAMETER ExpectedValue
        The expected value to compare the type against.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.Boolean
 
        Returns $true if the values have the same type, $false otherwise.
 
    .EXAMPLE
        Test-ObjectType -ActualValue 123 -ExpectedValue 456
 
        Returns $true because both values are [System.Int32].
 
    .EXAMPLE
        Test-ObjectType -ActualValue 123 -ExpectedValue '123'
 
        Returns $false because the types differ ([System.Int32] vs [System.String]).
#>

function Test-ObjectType
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [System.Object]
        $ActualValue,

        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [System.Object]
        $ExpectedValue
    )

    # Handle null cases
    if ($null -eq $ActualValue -and $null -eq $ExpectedValue)
    {
        # Both null - types match
        return $true
    }

    if ($null -eq $ActualValue -or $null -eq $ExpectedValue)
    {
        # One is null, the other is not - types don't match
        return $false
    }

    # Both are non-null, compare types
    try
    {
        $actualType = $ActualValue.GetType()
    }
    catch
    {
        $errorMessage = $script:localizedData.Test_ObjectType_GetTypeFailed -f $script:localizedData.Common_WordActual

        $PSCmdlet.ThrowTerminatingError(
            [System.Management.Automation.ErrorRecord]::new(
                [System.InvalidOperationException]::new($errorMessage),
                'TOT0001',
                [System.Management.Automation.ErrorCategory]::InvalidOperation,
                $ActualValue
            )
        )
    }

    try
    {
        $expectedType = $ExpectedValue.GetType()
    }
    catch
    {
        $errorMessage = $script:localizedData.Test_ObjectType_GetTypeFailed -f $script:localizedData.Common_WordExpected

        $PSCmdlet.ThrowTerminatingError(
            [System.Management.Automation.ErrorRecord]::new(
                [System.InvalidOperationException]::new($errorMessage),
                'TOT0001',
                [System.Management.Automation.ErrorCategory]::InvalidOperation,
                $ExpectedValue
            )
        )
    }

    return $actualType -eq $expectedType
}
#EndRegion '.\Private\Test-ObjectType.ps1' 103
#Region '.\Private\Test-ValueEquality.ps1' -1

<#
    .SYNOPSIS
        Tests whether two values are equal.
 
    .DESCRIPTION
        The `Test-ValueEquality` function performs equality comparison between
        two values, handling special cases such as null values and arrays.
        This function assumes type compatibility has already been validated
        when strict type checking is required.
 
    .PARAMETER ActualValue
        The actual value to compare.
 
    .PARAMETER ExpectedValue
        The expected value to compare against.
 
    .INPUTS
        None. This function does not accept pipeline input.
 
    .OUTPUTS
        System.Boolean
 
        Returns $true if the values are equal, $false otherwise.
 
    .EXAMPLE
        Test-ValueEquality -ActualValue $actual -ExpectedValue $expected
 
        Compares $actual and $expected values for equality.
 
    .EXAMPLE
        Test-ValueEquality -ActualValue @(1, 2, 3) -ExpectedValue @(1, 2, 3)
 
        Returns $true because the arrays have the same elements in the same order.
#>

function Test-ValueEquality
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [System.Object]
        $ActualValue,

        [Parameter(Mandatory = $true)]
        [AllowNull()]
        [System.Object]
        $ExpectedValue
    )

    $valuesAreEqual = $false

    if ($null -eq $ActualValue -and $null -eq $ExpectedValue)
    {
        $valuesAreEqual = $true
    }
    elseif ($null -eq $ActualValue -or $null -eq $ExpectedValue)
    {
        # One is null and the other is not
        $valuesAreEqual = $false
    }
    elseif ($ActualValue -is [System.Array] -and $ExpectedValue -is [System.Array])
    {
        # Use StructuralEqualityComparer for element-wise array comparison (supports value-type arrays)
        $valuesAreEqual = [System.Collections.StructuralComparisons]::StructuralEqualityComparer.Equals($ActualValue, $ExpectedValue)
    }
    else
    {
        # Use PowerShell's built-in comparison
        $valuesAreEqual = $ActualValue -eq $ExpectedValue
    }

    return $valuesAreEqual
}
#EndRegion '.\Private\Test-ValueEquality.ps1' 76
#Region '.\Public\Assert-BlockString.ps1' -1

<#
    .SYNOPSIS
        Asserts that a string, here-string, or array of strings matches the expected
        value.
 
    .DESCRIPTION
        The `Assert-BlockString` command compares a string, here-string, or array of
        strings against an expected string, here-string, or array of strings. If
        they are not identical, it throws an error that includes the hex output.
        The comparison is case-sensitive. It is commonly used in unit testing
        scenarios to verify the correctness of string outputs at the byte level.
 
    .PARAMETER Actual
        The actual string, here-string, or array of strings to be compared with
        the expected value. This parameter accepts pipeline input.
 
    .PARAMETER Expected
        The expected string, here-string, or array of strings that the actual value
        should match.
 
    .PARAMETER Because
        An optional reason or explanation for the assertion.
 
    .PARAMETER Highlight
        An optional ANSI color code to highlight the difference between the expected
        and actual strings. The default value is '31m' (red text).
 
    .PARAMETER NoHexOutput
        Specifies whether to omit the hex columns and output only the character groups.
 
    .INPUTS
        System.Object
 
        Accepts strings or arrays of strings via the pipeline.
 
    .OUTPUTS
        None. This command does not return any output on success.
 
    .EXAMPLE
        PS> Assert-BlockString -Actual 'hello', 'world' -Expected 'Hello', 'World'
 
        This example asserts that the array of strings 'hello' and 'world' matches
        the expected array of strings 'Hello' and 'World'. If the assertion fails,
        an error is thrown.
 
    .EXAMPLE
        PS> 'hello', 'world' | Assert-BlockString -Expected 'Hello', 'World'
 
        This example demonstrates the usage of pipeline input. A string array containing
        'hello' and 'world' are piped to `Assert-BlockString` and compared with the
        expected string array containing 'Hello' and 'World'. If the assertion fails,
        an error is thrown.
 
    .NOTES
        TODO: Is it possible to rename the command to `Should-BeBlockString`? Pester
        handles commands with the `Should` verb; however, it is unclear if this issue
        can be resolved here. See:
        https://github.com/pester/Pester/commit/c8bc9679bed19c8fbc4229caa01dd083f2d03d4f#diff-b7592dd925696de2521c9b12b966d65519d502045462f002c343caa7c0986936
        and
        https://github.com/pester/Pester/commit/c8bc9679bed19c8fbc4229caa01dd083f2d03d4f#diff-460f64eafc16facefbed201eb00fb151c75eadf7cc58a504a01527015fb1c7cdR17
#>

function Assert-BlockString
{
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples are syntactically correct. The rule does not seem to understand that there is pipeline input.')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidThrowOutsideOfTry', '')]
    [CmdletBinding()]
    [Alias('Should-BeBlockString')]
    [OutputType()]
    param
    (
        [Parameter(Position = 1, Mandatory = $true, ValueFromPipeline = $true)]
        [System.Object]
        $Actual,

        [Parameter(Position = 0, Mandatory = $true)]
        [System.Object]
        $Expected,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Because,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Highlight = '31m',

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $NoHexOutput
    )

    $hasPipelineInput = $MyInvocation.ExpectingInput

    if ($hasPipelineInput)
    {
        $Actual = @($local:Input)
    }

    # Verify if $Actual is a string or string array
    $isActualStringType = $Actual -is [System.String] -or ($Actual -is [System.Array] -and ($Actual.Count -eq 0 -or ($Actual.Count -gt 0 -and $Actual[0] -is [System.String])))

    $isExpectedStringType = $Expected -is [System.String] -or ($Expected -is [System.Array] -and ($Expected.Count -eq 0 -or ($Expected.Count -gt 0 -and $Expected[0] -is [System.String])))

    $stringsAreEqual = $isActualStringType -and $isExpectedStringType -and (-join $Actual) -ceq (-join $Expected)

    if (-not $stringsAreEqual)
    {
        if (-not $isActualStringType)
        {
            $message = $script:localizedData.Assert_BlockString_ActualInvalid
        }
        elseif (-not $isExpectedStringType)
        {
            $message = $script:localizedData.Assert_BlockString_ExpectedInvalid
        }
        else
        {
            $message = $script:localizedData.Assert_BlockString_StringsNotEqual

            $message += "{0}`r`n " -f $script:localizedData.Assert_BlockString_Difference

            $message += Out-Difference -Reference $Expected -Difference $Actual -ReferenceLabel 'Expected:' -DifferenceLabel 'But was:' -HighlightStart:$Highlight -NoHexOutput:$NoHexOutput.IsPresent |
                ForEach-Object -Process { "`e[0m$_`r`n" }
        }

        throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
    }
}
#EndRegion '.\Public\Assert-BlockString.ps1' 132
#Region '.\Public\Assert-ObjectMethod.ps1' -1

<#
    .SYNOPSIS
        Asserts that an object contains a specified method.
 
    .DESCRIPTION
        The `Assert-ObjectMethod` command verifies that an object contains a
        specified method. This is commonly used in unit testing scenarios to
        verify object structure and ensure that required methods are available
        on objects before attempting to invoke them.
 
    .PARAMETER Method
        The name of the method to assert exists on the object.
 
    .PARAMETER Actual
        The object to be inspected for the method. This parameter accepts
        pipeline input.
 
    .PARAMETER Because
        An optional reason or explanation for the assertion.
 
    .PARAMETER Each
        When specified and the input is an array, asserts that each element in
        the array has the specified method. Without this parameter, the assertion
        checks the array object itself.
 
    .INPUTS
        System.Object
 
        Accepts any object via the pipeline for method inspection.
 
    .OUTPUTS
        None
 
        This command does not return any output on success.
 
    .EXAMPLE
        PS> Assert-ObjectMethod -Actual $myObject -Method 'ToString'
 
        This example asserts that the object in `$myObject` has a method named
        'ToString'. If the method does not exist, an error is thrown.
 
    .EXAMPLE
        PS> $myObject | Assert-ObjectMethod -Method 'GetHashCode'
 
        This example demonstrates pipeline usage. The object `$myObject` is piped
        to `Assert-ObjectMethod` and checked for a method named 'GetHashCode'.
 
    .EXAMPLE
        PS> Assert-ObjectMethod -Method 'Connect' -Actual $connection -Because 'the connection object should support Connect method'
 
        This example asserts that `$connection` has a method named 'Connect',
        providing a reason for the assertion.
 
    .EXAMPLE
        PS> $collection | Assert-ObjectMethod -Method 'Add'
 
        This example verifies that a collection object has an 'Add' method,
        which is useful when you need to ensure you can add items to a collection.
 
    .EXAMPLE
        PS> $arrayOfObjects | Assert-ObjectMethod -Method 'ToString' -Each
 
        This example asserts that each object in the piped array has a 'ToString'
        method. The `-Each` parameter enables element-by-element checking.
#>

function Assert-ObjectMethod
{
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples are syntactically correct. The rule does not seem to understand that there is pipeline input.')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidThrowOutsideOfTry', '')]
    [CmdletBinding()]
    [Alias('Should-HaveMethod')]
    [OutputType()]
    param
    (
        [Parameter(Position = 0, Mandatory = $true)]
        [System.String]
        $Method,

        [Parameter(Position = 1, Mandatory = $true, ValueFromPipeline = $true)]
        [System.Object]
        $Actual,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Because,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $Each
    )

    $hasPipelineInput = $MyInvocation.ExpectingInput

    if ($hasPipelineInput)
    {
        $Actual = @($local:Input)

        # If we're not using -Each and we have a single-element array, unwrap it
        # This handles the case where a single object is piped
        if (-not $Each.IsPresent -and $Actual.Count -eq 1)
        {
            $Actual = $Actual[0]
        }
    }

    # If Each is specified and we have an array, iterate through each element
    if ($Each.IsPresent -and $Actual -is [System.Array] -and $Actual.Count -gt 0)
    {
        foreach ($currentObject in $Actual)
        {
            # Check if the current object is null
            if ($null -eq $currentObject)
            {
                $message = $script:localizedData.Assert_ObjectMethod_ActualIsNull
                throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
            }

            # Check if the method exists on the current object
            $hasMethod = Test-ObjectHasMethod -InputObject $currentObject -MethodName $Method

            if (-not $hasMethod)
            {
                $message = $script:localizedData.Assert_ObjectMethod_MethodNotFound -f $Method
                throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
            }
        }
    }
    else
    {
        # Single object case (direct parameter or not from pipeline)
        # Check if the actual value is null
        if ($null -eq $Actual)
        {
            $message = $script:localizedData.Assert_ObjectMethod_ActualIsNull
            throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
        }

        # Check if the method exists on the object
        $hasMethod = Test-ObjectHasMethod -InputObject $Actual -MethodName $Method

        if (-not $hasMethod)
        {
            $message = $script:localizedData.Assert_ObjectMethod_MethodNotFound -f $Method
            throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
        }
    }
}
#EndRegion '.\Public\Assert-ObjectMethod.ps1' 150
#Region '.\Public\Assert-ObjectProperty.ps1' -1

<#
    .SYNOPSIS
        Asserts that an object contains a specified property.
 
    .DESCRIPTION
        The `Assert-ObjectProperty` command verifies that an object contains a
        specified property. It can optionally also verify that the property has
        a specific value. This is commonly used in unit testing scenarios to
        verify object structure and property values.
 
    .PARAMETER Property
        The name of the property to assert exists on the object.
 
    .PARAMETER Actual
        The object to be inspected for the property. This parameter accepts
        pipeline input.
 
    .PARAMETER Value
        The expected value of the property. If specified, the assertion will
        check both property existence and value equality.
 
    .PARAMETER Because
        An optional reason or explanation for the assertion.
 
    .PARAMETER Each
        When specified and the input is an array, asserts that each element in
        the array has the specified property (and optionally the specified value).
        Without this parameter, the assertion checks the array object itself.
 
    .PARAMETER NoTypeCheck
        When specified, allows PowerShell type coercion when comparing values
        (e.g., allows 123 to equal '123'). By default, value comparisons use
        strict type checking where types must match exactly. This parameter
        only applies when using the Value parameter.
 
    .INPUTS
        System.Object
 
        Accepts any object via the pipeline for property inspection.
 
    .OUTPUTS
        None
 
        This command does not return any output on success.
 
    .EXAMPLE
        PS> Assert-ObjectProperty -Actual $myObject -Property 'Enabled'
 
        This example asserts that the object in `$myObject` has a property named
        'Enabled'. If the property does not exist, an error is thrown.
 
    .EXAMPLE
        PS> Assert-ObjectProperty -Actual $myObject -Property 'Enabled' -Value $true
 
        This example asserts that the object in `$myObject` has a property named
        'Enabled' with the value `$true`. If the property does not exist or the
        value does not match, an error is thrown.
 
    .EXAMPLE
        PS> $myObject | Assert-ObjectProperty -Property 'Status' -Value 'Running'
 
        This example demonstrates pipeline usage. The object `$myObject` is piped
        to `Assert-ObjectProperty` and checked for a property named 'Status' with
        the value 'Running'.
 
    .EXAMPLE
        PS> Assert-ObjectProperty -Property 'Count' -Actual $collection -Value 5 -Because 'the collection should contain exactly 5 items'
 
        This example asserts that `$collection` has a property named 'Count' with
        the value 5, providing a reason for the assertion.
 
    .EXAMPLE
        PS> $arrayOfObjects | Assert-ObjectProperty -Property 'Name' -Each
 
        This example asserts that each object in the piped array has a property
        named 'Name'. The `-Each` parameter enables element-by-element checking.
 
    .EXAMPLE
        Assert-ObjectProperty -Actual $myObject -Property 'Value' -Value 123 -NoTypeCheck
 
        This example uses lenient type checking, so if the Value property contains
        the string '123', it will be considered equal to the number 123.
#>

function Assert-ObjectProperty
{
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('UseSyntacticallyCorrectExamples', '', Justification = 'Because the examples are syntactically correct. The rule does not seem to understand that there is pipeline input.')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand', '')]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('AvoidThrowOutsideOfTry', '')]
    [CmdletBinding(DefaultParameterSetName = 'AssertProperty')]
    [Alias('Should-HaveProperty')]
    [OutputType()]
    param
    (
        [Parameter(ParameterSetName = 'AssertProperty', Position = 0, Mandatory = $true)]
        [Parameter(ParameterSetName = 'AssertValue', Position = 0, Mandatory = $true)]
        [System.String]
        $Property,

        [Parameter(ParameterSetName = 'AssertProperty', Position = 1, Mandatory = $true, ValueFromPipeline = $true)]
        [Parameter(ParameterSetName = 'AssertValue', Position = 2, Mandatory = $true, ValueFromPipeline = $true)]
        [System.Object]
        $Actual,

        [Parameter(ParameterSetName = 'AssertValue', Position = 1, Mandatory = $true)]
        [AllowNull()]
        [System.Object]
        $Value,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $Because,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $Each,

        [Parameter()]
        [System.Management.Automation.SwitchParameter]
        $NoTypeCheck
    )

    $hasPipelineInput = $MyInvocation.ExpectingInput

    if ($hasPipelineInput)
    {
        $Actual = @($local:Input)

        # If we're not using -Each and we have a single-element array, unwrap it
        # This handles the case where a single hashtable or object is piped
        if (-not $Each.IsPresent -and $Actual.Count -eq 1)
        {
            $Actual = $Actual[0]
        }
    }

    # If Each is specified and we have an array, iterate through each element
    if ($Each.IsPresent -and $Actual -is [System.Array] -and $Actual.Count -gt 0)
    {
        foreach ($currentObject in $Actual)
        {
            # Check if the current object is null
            if ($null -eq $currentObject)
            {
                $message = $script:localizedData.Assert_ObjectProperty_ActualIsNull
                throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
            }

            # Check if the property exists on the current object
            $hasProperty = Test-ObjectHasProperty -InputObject $currentObject -PropertyName $Property

            if (-not $hasProperty)
            {
                $message = $script:localizedData.Assert_ObjectProperty_PropertyNotFound -f $Property
                throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
            }

            # If we're in the AssertValue parameter set, also check the value
            if ($PSCmdlet.ParameterSetName -eq 'AssertValue')
            {
                $actualValue = $currentObject.$Property

                # Check type compatibility first (unless NoTypeCheck is specified)
                if (-not $NoTypeCheck.IsPresent)
                {
                    $typesMatch = Test-ObjectType -ActualValue $actualValue -ExpectedValue $Value

                    if (-not $typesMatch)
                    {
                        $actualType = Get-TypeName -Value $actualValue
                        $expectedType = Get-TypeName -Value $Value
                        $message = $script:localizedData.Assert_ObjectProperty_TypeMismatch -f $Property, $expectedType, $actualType
                        throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
                    }
                }

                # Then check value equality
                $valuesAreEqual = Test-ValueEquality -ActualValue $actualValue -ExpectedValue $Value

                if (-not $valuesAreEqual)
                {
                    $message = $script:localizedData.Assert_ObjectProperty_ValueMismatch -f $Property, $Value, $actualValue
                    throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
                }
            }
        }
    }
    else
    {
        # Single object case (not an array or empty array)
        # Check if the actual value is null
        if ($null -eq $Actual)
        {
            $message = $script:localizedData.Assert_ObjectProperty_ActualIsNull
            throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
        }

        # Check if the property exists on the object
        $hasProperty = Test-ObjectHasProperty -InputObject $Actual -PropertyName $Property

        if (-not $hasProperty)
        {
            $message = $script:localizedData.Assert_ObjectProperty_PropertyNotFound -f $Property
            throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
        }

        # If we're in the AssertValue parameter set, also check the value
        if ($PSCmdlet.ParameterSetName -eq 'AssertValue')
        {
            $actualValue = $Actual.$Property

            # Check type compatibility first (unless NoTypeCheck is specified)
            if (-not $NoTypeCheck.IsPresent)
            {
                $typesMatch = Test-ObjectType -ActualValue $actualValue -ExpectedValue $Value

                if (-not $typesMatch)
                {
                    $actualType = Get-TypeName -Value $actualValue
                    $expectedType = Get-TypeName -Value $Value
                    $message = $script:localizedData.Assert_ObjectProperty_TypeMismatch -f $Property, $expectedType, $actualType
                    throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
                }
            }

            # Then check value equality
            $valuesAreEqual = Test-ValueEquality -ActualValue $actualValue -ExpectedValue $Value

            if (-not $valuesAreEqual)
            {
                $message = $script:localizedData.Assert_ObjectProperty_ValueMismatch -f $Property, $Value, $actualValue
                throw (New-AssertionError -Message $message -Because $Because -InvocationInfo $MyInvocation)
            }
        }
    }
}
#EndRegion '.\Public\Assert-ObjectProperty.ps1' 237