Tests/Public/Compare-FileShareIntegrity.Tests.ps1

[Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingEmptyCatchBlock', '', Justification = 'Tests intentionally swallow errors to verify side-effects')]
param()

BeforeAll {
    $modulePath = "$PSScriptRoot/../.."
    Get-ChildItem -Path "$modulePath/Private/*.ps1" | ForEach-Object { . $_.FullName }
    Get-ChildItem -Path "$modulePath/Public/*.ps1"  | ForEach-Object { . $_.FullName }
}

Describe 'Compare-FileShareIntegrity' {
    BeforeEach {
        Mock Write-Host {}
        Mock Write-Warning {}
        Mock Assert-AzCliLogin { [PSCustomObject]@{ user = @{ name = 'test@contoso.com' } } }
        Mock Get-StorageAccountKey { 'fakekey123==' }
        Mock Mount-SmbDrive {}
        Mock Dismount-SmbDrive {}
    }

    Context 'Mutually exclusive deep-check modes' {
        It 'Throws when -Partial and -HashAll are both specified' {
            { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -Partial -HashAll } | Should -Throw '*Only one of*'
        }

        It 'Throws when -Partial and -SampleCount are both specified' {
            { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -Partial -SampleCount 100 } | Should -Throw '*Only one of*'
        }

        It 'Throws when -HashAll and -SampleCount are both specified' {
            { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -HashAll -SampleCount 100 } | Should -Throw '*Only one of*'
        }
    }

    Context 'Drive letter validation' {
        It 'Throws when source and destination drive letters are the same' {
            { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' -SourceDriveLetter 'X' -DestinationDriveLetter 'X' } | Should -Throw '*must be different*'
        }

        It 'Throws when drive is already mounted' {
            Mock Test-Path { $true } -ParameterFilter { $Path -like '*:\' }
            { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' } | Should -Throw '*already mounted*'
        }
    }

    Context 'Pre-flight calls' {
        BeforeEach {
            Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' }
            # Make Mount-SmbDrive throw to stop execution before file enumeration
            Mock Mount-SmbDrive { throw 'Intentional stop' }
        }

        It 'Calls Assert-AzCliLogin' {
            try { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' } catch {}
            Should -Invoke Assert-AzCliLogin -Times 1
        }

        It 'Retrieves keys for both accounts' {
            try { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' } catch {}
            Should -Invoke Get-StorageAccountKey -Times 2
        }
    }

    Context 'Share resolution' {
        BeforeEach {
            Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' }
            # Throw on mount so enumeration never starts
            Mock Mount-SmbDrive { throw 'Intentional stop' }
        }

        It 'Lists all shares when -ShareName is not provided' {
            Mock -CommandName 'az' -MockWith {
                $global:LASTEXITCODE = 0
                'share1'
                'share2'
            }
            try { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' } catch {}
            Should -Invoke Mount-SmbDrive -Times 1 -ParameterFilter { $ShareName -eq 'share1' }
        }
    }

    Context 'Cleanup on failure' {
        BeforeEach {
            Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' }
            Mock Mount-SmbDrive { throw 'Mount failed' } -ParameterFilter { $DriveLetter -eq 'Y' }
            Mock Mount-SmbDrive {} -ParameterFilter { $DriveLetter -eq 'X' }
        }

        It 'Dismounts drives even when an error occurs' {
            try { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' } catch {}
            Should -Invoke Dismount-SmbDrive
        }
    }

    Context 'Parameter validation' {
        It 'ThrottleLimit must be between 1 and 64' {
            { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ThrottleLimit 0 } | Should -Throw
            { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ThrottleLimit 65 } | Should -Throw
        }

        It 'SampleCount must be positive' {
            { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -SampleCount 0 } | Should -Throw
        }

        It 'Drive letter must be a single alpha character' {
            { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -SourceDriveLetter '12' } | Should -Throw
        }

        It 'SourceAccountName is mandatory' {
            $param = (Get-Command Compare-FileShareIntegrity).Parameters['SourceAccountName']
            $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | ForEach-Object {
                $_.Mandatory | Should -BeTrue
            }
        }

        It 'DestinationAccountName is mandatory' {
            $param = (Get-Command Compare-FileShareIntegrity).Parameters['DestinationAccountName']
            $param.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } | ForEach-Object {
                $_.Mandatory | Should -BeTrue
            }
        }

        It 'SupportsShouldProcess is enabled' {
            $cmd = Get-Command Compare-FileShareIntegrity
            $cmdBindingAttr = $cmd.ScriptBlock.Attributes | Where-Object { $_ -is [System.Management.Automation.CmdletBindingAttribute] }
            $cmdBindingAttr.SupportsShouldProcess | Should -BeTrue
        }
    }

    Context 'Share list failure' {
        BeforeEach {
            Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' }
            Mock -CommandName 'az' -MockWith {
                $global:LASTEXITCODE = 1
                'AuthorizationFailure'
            }
        }

        It 'Throws when share list retrieval fails' {
            { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' } | Should -Throw '*Failed to list*'
        }
    }

    Context 'Empty share list' {
        BeforeEach {
            Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' }
            Mock -CommandName 'az' -MockWith {
                $global:LASTEXITCODE = 0
                ''
            }
        }

        It 'Throws when no shares are found on source' {
            { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' } | Should -Throw '*No file shares found*'
        }
    }

    Context 'File comparison with real files via subst drives' {
        BeforeAll {
            # Pick two unused drive letters for subst
            $script:srcLetter = 'V'
            $script:dstLetter = 'W'

            # Prepare temp directories with test files
            $script:srcDir = Join-Path $TestDrive 'src_share'
            $script:dstDir = Join-Path $TestDrive 'dst_share'
            New-Item -ItemType Directory -Path $script:srcDir -Force | Out-Null
            New-Item -ItemType Directory -Path $script:dstDir -Force | Out-Null

            # identical file
            Set-Content -Path (Join-Path $script:srcDir 'identical.txt') -Value 'hello world' -NoNewline
            Set-Content -Path (Join-Path $script:dstDir 'identical.txt') -Value 'hello world' -NoNewline

            # size mismatch file
            Set-Content -Path (Join-Path $script:srcDir 'sizemismatch.txt') -Value 'short' -NoNewline
            Set-Content -Path (Join-Path $script:dstDir 'sizemismatch.txt') -Value 'much longer content' -NoNewline

            # missing on destination
            Set-Content -Path (Join-Path $script:srcDir 'missing_dest.txt') -Value 'only on source' -NoNewline

            # missing on source
            Set-Content -Path (Join-Path $script:dstDir 'missing_src.txt') -Value 'only on dest' -NoNewline

            # subdirectory with file
            New-Item -ItemType Directory -Path (Join-Path $script:srcDir 'sub') -Force | Out-Null
            New-Item -ItemType Directory -Path (Join-Path $script:dstDir 'sub') -Force | Out-Null
            Set-Content -Path (Join-Path $script:srcDir 'sub\nested.txt') -Value 'nested' -NoNewline
            Set-Content -Path (Join-Path $script:dstDir 'sub\nested.txt') -Value 'nested' -NoNewline

            # Create subst drives
            & subst "${script:srcLetter}:" $script:srcDir 2>$null
            & subst "${script:dstLetter}:" $script:dstDir 2>$null
        }

        AfterAll {
            & subst "${script:srcLetter}:" /D 2>$null
            & subst "${script:dstLetter}:" /D 2>$null
        }

        BeforeEach {
            # Override Test-Path mock to return false for our drive letters (bypass "already mounted" check)
            Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' }
        }

        It 'Produces CSV report with file comparison results for a named share' {
            $logDir = Join-Path $TestDrive 'logs_named'
            New-Item -ItemType Directory -Path $logDir -Force | Out-Null

            Compare-FileShareIntegrity `
                -SourceAccountName 'src' `
                -DestinationAccountName 'dst' `
                -ShareName 'test' `
                -SourceDriveLetter $script:srcLetter `
                -DestinationDriveLetter $script:dstLetter `
                -LogDirectory $logDir

            # Should have mounted and dismounted
            Should -Invoke Mount-SmbDrive -Times 2
            Should -Invoke Dismount-SmbDrive -Times 2

            # Find the CSV report
            $csvFiles = Get-ChildItem -Path $logDir -Recurse -Filter '*.csv'
            $csvFiles.Count | Should -BeGreaterOrEqual 1
            $csvContent = Import-Csv -Path $csvFiles[0].FullName
            $statuses = $csvContent.Status | Sort-Object -Unique

            # Should detect size mismatch and missing files
            $statuses | Should -Contain 'SizeMismatch'
            $statuses | Should -Contain 'MissingOnDestination'
            $statuses | Should -Contain 'MissingOnSource'
        }

        It 'Reports WhatIf for deep check instead of actually running it' {
            $logDir = Join-Path $TestDrive 'logs_whatif'
            New-Item -ItemType Directory -Path $logDir -Force | Out-Null

            Compare-FileShareIntegrity `
                -SourceAccountName 'src' `
                -DestinationAccountName 'dst' `
                -ShareName 'test' `
                -SourceDriveLetter $script:srcLetter `
                -DestinationDriveLetter $script:dstLetter `
                -LogDirectory $logDir `
                -HashAll `
                -WhatIf

            # Should write WhatIf message about deep check
            Should -Invoke Write-Host -ParameterFilter { $Object -like '*WhatIf*deep-check*' } -Times 1
        }

        It 'Runs with -Partial deep check mode (no parallel needed for 0 candidates when sizes differ)' {
            # Create a scenario where no same-size candidates exist → deep check has 0 items
            $logDir = Join-Path $TestDrive 'logs_partial'
            New-Item -ItemType Directory -Path $logDir -Force | Out-Null

            # This will run but the Parallel block won't execute for 0 items
            Compare-FileShareIntegrity `
                -SourceAccountName 'src' `
                -DestinationAccountName 'dst' `
                -ShareName 'test' `
                -SourceDriveLetter $script:srcLetter `
                -DestinationDriveLetter $script:dstLetter `
                -LogDirectory $logDir `
                -Partial

            Should -Invoke Mount-SmbDrive -Times 2
        }

        It 'Handles None deep-check mode and shows message' {
            $logDir = Join-Path $TestDrive 'logs_none'
            New-Item -ItemType Directory -Path $logDir -Force | Out-Null

            Compare-FileShareIntegrity `
                -SourceAccountName 'src' `
                -DestinationAccountName 'dst' `
                -ShareName 'test' `
                -SourceDriveLetter $script:srcLetter `
                -DestinationDriveLetter $script:dstLetter `
                -LogDirectory $logDir

            # "No deep-check mode specified" message
            Should -Invoke Write-Host -ParameterFilter { $Object -like '*No deep-check mode*' } -Times 1
        }

        It 'Handles Sample deep-check mode with count larger than candidates' {
            $logDir = Join-Path $TestDrive 'logs_sample'
            New-Item -ItemType Directory -Path $logDir -Force | Out-Null

            # SampleCount = 1000, but only 2 same-size files exist, so it will take all
            Compare-FileShareIntegrity `
                -SourceAccountName 'src' `
                -DestinationAccountName 'dst' `
                -ShareName 'test' `
                -SourceDriveLetter $script:srcLetter `
                -DestinationDriveLetter $script:dstLetter `
                -LogDirectory $logDir `
                -SampleCount 1000

            Should -Invoke Mount-SmbDrive -Times 2
        }
    }

    Context 'Multiple shares - grand summary' {
        BeforeAll {
            $script:srcLetter2 = 'V'
            $script:dstLetter2 = 'W'

            # Two shares in separate directories
            $script:srcBase = Join-Path $TestDrive 'multi_src'
            $script:dstBase = Join-Path $TestDrive 'multi_dst'
            New-Item -ItemType Directory -Path $script:srcBase -Force | Out-Null
            New-Item -ItemType Directory -Path $script:dstBase -Force | Out-Null

            # Files for share1
            Set-Content -Path (Join-Path $script:srcBase 'file1.txt') -Value 'content1' -NoNewline
            Set-Content -Path (Join-Path $script:dstBase 'file1.txt') -Value 'content1' -NoNewline

            # Create subst (reuse same from previous context if still around)
            & subst "${script:srcLetter2}:" /D 2>$null
            & subst "${script:dstLetter2}:" /D 2>$null
            & subst "${script:srcLetter2}:" $script:srcBase
            & subst "${script:dstLetter2}:" $script:dstBase
        }

        AfterAll {
            & subst "${script:srcLetter2}:" /D 2>$null
            & subst "${script:dstLetter2}:" /D 2>$null
        }

        BeforeEach {
            Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' }
            Mock -CommandName 'az' -MockWith {
                $global:LASTEXITCODE = 0
                'share1'
                'share2'
            }
        }

        It 'Produces grand summary when processing multiple shares' {
            $logDir = Join-Path $TestDrive 'logs_multi'
            New-Item -ItemType Directory -Path $logDir -Force | Out-Null

            Compare-FileShareIntegrity `
                -SourceAccountName 'src' `
                -DestinationAccountName 'dst' `
                -SourceDriveLetter $script:srcLetter2 `
                -DestinationDriveLetter $script:dstLetter2 `
                -LogDirectory $logDir

            # Grand summary header should be printed
            Should -Invoke Write-Host -ParameterFilter { $Object -like '*GRAND SUMMARY*' } -Times 1
            # Both shares were attempted
            Should -Invoke Mount-SmbDrive -Times 4   # 2 per share × 2 shares
        }
    }

    Context 'Error handling during share processing' {
        BeforeAll {
            $script:srcLetterE = 'V'
            $script:dstLetterE = 'W'
            $script:srcDirE = Join-Path $TestDrive 'err_src'
            New-Item -ItemType Directory -Path $script:srcDirE -Force | Out-Null
            Set-Content -Path (Join-Path $script:srcDirE 'f.txt') -Value 'x' -NoNewline

            & subst "${script:srcLetterE}:" /D 2>$null
            & subst "${script:dstLetterE}:" /D 2>$null
            & subst "${script:srcLetterE}:" $script:srcDirE
            # Intentionally do NOT create W: subst → EnumerateFiles will fail
        }

        AfterAll {
            & subst "${script:srcLetterE}:" /D 2>$null
            & subst "${script:dstLetterE}:" /D 2>$null
        }

        BeforeEach {
            Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' }
        }

        It 'Re-throws when single share processing fails' {
            { Compare-FileShareIntegrity `
                -SourceAccountName 'src' `
                -DestinationAccountName 'dst' `
                -ShareName 'test' `
                -SourceDriveLetter $script:srcLetterE `
                -DestinationDriveLetter $script:dstLetterE `
                -LogDirectory $TestDrive } | Should -Throw
            Should -Invoke Dismount-SmbDrive -Times 1  # only source was mounted
        }
    }

    Context 'Cleanup on mount failure' {
        BeforeEach {
            Mock Test-Path { $false } -ParameterFilter { $Path -like '*:\' }
            Mock Mount-SmbDrive { throw 'mount error' } -ParameterFilter { $DriveLetter -eq 'Y' }
            Mock Mount-SmbDrive {} -ParameterFilter { $DriveLetter -eq 'X' }
        }

        It 'Dismounts source but not destination when destination mount fails' {
            try { Compare-FileShareIntegrity -SourceAccountName 'src' -DestinationAccountName 'dst' -ShareName 'test' } catch {}
            Should -Invoke Dismount-SmbDrive -Times 1 -ParameterFilter { $DriveLetter -eq 'X' }
            Should -Invoke Dismount-SmbDrive -Times 0 -ParameterFilter { $DriveLetter -eq 'Y' }
        }
    }
}