tests/Private/Set-AzureTag.Tests.ps1

# Determine if Azure tests should be skipped (must be outside Describe)
$skipAzureTests = -not (Get-Command Get-AzTag -ErrorAction SilentlyContinue)

Describe 'Set-AzureTag' {
    BeforeAll {
        $ModulePath = $PSScriptRoot | Split-Path -Parent | Split-Path -Parent | Join-Path -ChildPath 'PSCumulus.psd1'
        Import-Module $ModulePath -Force
    }

    Context 'Parameter validation' {
        It 'Should have -ResourceId as mandatory parameter' {
            InModuleScope PSCumulus {
                $params = (Get-Command Set-AzureTag).Parameters
                $params.ContainsKey('ResourceId') | Should -BeTrue
                $params.ResourceId.Attributes.Mandatory | Should -BeTrue
            }
        }

        It 'Should have -Tags as mandatory parameter' {
            InModuleScope PSCumulus {
                $params = (Get-Command Set-AzureTag).Parameters
                $params.ContainsKey('Tags') | Should -BeTrue
                $params.Tags.Attributes.Mandatory | Should -BeTrue
            }
        }

        It 'Should have -Merge switch' {
            InModuleScope PSCumulus {
                $params = (Get-Command Set-AzureTag).Parameters
                $params.ContainsKey('Merge') | Should -BeTrue
                $params.Merge.ParameterType.Name | Should -Be 'SwitchParameter'
            }
        }
    }

    Context 'Command availability check' {
        It 'Should fail if Update-AzTag is not available' {
            InModuleScope PSCumulus {
                Mock -CommandName Assert-CommandAvailable -MockWith {
                    throw 'Update-AzTag not found'
                }

                { Set-AzureTag -ResourceId 'test-id' -Tags @{Key = 'Value' } -ErrorAction Stop } | Should -Throw
            }
        }

        It 'Should not throw when Update-AzTag is available' -Skip:$skipAzureTests {
            InModuleScope PSCumulus {
                Mock -CommandName Assert-CommandAvailable
                Mock -CommandName Get-AzTag -MockWith { $null }
                Mock -CommandName Update-AzTag -MockWith {
                    param($ResourceId, $Tag, $Operation)
                    @{Properties = @{ Tags = @{}} }
                }

                { Set-AzureTag -ResourceId 'test-id' -Tags @{Key = 'Value' } } | Should -Not -Throw
            }
        }
    }

    Context 'Tag merge behavior' {
        It 'Should merge with existing tags when -Merge is specified' -Skip:$skipAzureTests {
            InModuleScope PSCumulus {
                Mock -CommandName Assert-CommandAvailable
                Mock -CommandName Get-AzTag -MockWith {
                    @{
                        Properties = @{
                            Tags = @{
                                ExistingTag = 'ExistingValue'
                            }
                        }
                    }
                }
                Mock -CommandName Update-AzTag -MockWith {
                    param($ResourceId, $Tag, $Operation)
                    $Operation | Should -Be 'Merge'
                    # Only the new tags are passed to Update-AzTag; Azure API handles the merge
                    $Tag.ContainsKey('NewTag') | Should -BeTrue
                    $Tag['NewTag'] | Should -Be 'NewValue'
                    # ExistingTag is NOT in $Tag - the Azure API merges server-side
                }

                Set-AzureTag -ResourceId 'test-id' -Tags @{NewTag = 'NewValue'} -Merge
            }
        }

        It 'Should replace all tags when -Merge is not specified' -Skip:$skipAzureTests {
            InModuleScope PSCumulus {
                Mock -CommandName Assert-CommandAvailable
                Mock -CommandName Get-AzTag -MockWith {
                    @{
                        Properties = @{
                            Tags = @{
                                ExistingTag = 'ExistingValue'
                            }
                        }
                    }
                }
                Mock -CommandName Update-AzTag -MockWith {
                    param($ResourceId, $Tag, $Operation)
                    $Operation | Should -Be 'Replace'
                    $Tag.ContainsKey('ExistingTag') | Should -BeFalse
                    $Tag.ContainsKey('NewTag') | Should -BeTrue
                }

                Set-AzureTag -ResourceId 'test-id' -Tags @{NewTag = 'NewValue'}
            }
        }
    }

    Context 'Error handling' {
        It 'Should handle missing existing tags gracefully' -Skip:$skipAzureTests {
            InModuleScope PSCumulus {
                Mock -CommandName Assert-CommandAvailable
                Mock -CommandName Get-AzTag -MockWith { $null }
                Mock -CommandName Update-AzTag -MockWith {
                    param($ResourceId, $Tag, $Operation)
                    @{Properties = @{ Tags = @{}} }
                }

                { Set-AzureTag -ResourceId 'test-id' -Tags @{Key = 'Value' } } | Should -Not -Throw
            }
        }

        It 'Should stop on Update-AzTag errors' -Skip:$skipAzureTests {
            InModuleScope PSCumulus {
                Mock -CommandName Assert-CommandAvailable
                Mock -CommandName Get-AzTag -MockWith { $null }
                Mock -CommandName Update-AzTag -MockWith {
                    param($ResourceId, $Tag, $Operation)
                    throw 'Update failed'
                }

                { Set-AzureTag -ResourceId 'test-id' -Tags @{Key = 'Value' } -ErrorAction Stop } | Should -Throw
            }
        }
    }
}