modules/Azure/Discovery/Tests/Unit/StartCIEMAzureDiscovery.Tests.ps1

BeforeAll {
    Remove-Module Devolutions.CIEM -Force -ErrorAction SilentlyContinue
    Import-Module (Join-Path $PSScriptRoot '..' '..' '..' '..' '..' 'Devolutions.CIEM.psd1')
    Mock -ModuleName Devolutions.CIEM Write-CIEMLog {}

    # Create isolated test DB with base + azure + discovery schemas
    New-CIEMDatabase -Path "$TestDrive/ciem.db"

    InModuleScope Devolutions.CIEM {
        $script:DatabasePath = "$TestDrive/ciem.db"
    }

    foreach ($schemaPath in @(
        (Join-Path $PSScriptRoot '..' '..' '..' 'Infrastructure' 'Data' 'azure_schema.sql'),
        (Join-Path $PSScriptRoot '..' '..' 'Data' 'discovery_schema.sql')
    )) {
        foreach ($statement in ((Get-Content $schemaPath -Raw) -split ';\s*\n' | Where-Object { $_.Trim() })) {
            $trimmed = $statement.Trim()
            try {
                Invoke-CIEMQuery -Query $trimmed -AsNonQuery | Out-Null
            }
            catch {
                if ($trimmed -match 'ALTER\s+TABLE' -and $_.Exception.Message -match 'duplicate column') {
                    continue
                }
                throw
            }
        }
    }
}

Describe 'Start-CIEMAzureDiscovery' {

    Context 'Command structure' {
        It 'Start-CIEMAzureDiscovery is available as a public command' {
            Get-Command -Module Devolutions.CIEM -Name Start-CIEMAzureDiscovery -ErrorAction Stop | Should -Not -BeNullOrEmpty
        }

        It 'Accepts -Scope parameter with ValidateSet All, ARM, Entra' {
            $cmd = Get-Command -Module Devolutions.CIEM -Name Start-CIEMAzureDiscovery
            $scopeParam = $cmd.Parameters['Scope']
            $scopeParam | Should -Not -BeNullOrEmpty
            $validateSet = $scopeParam.Attributes | Where-Object { $_ -is [System.Management.Automation.ValidateSetAttribute] }
            $validateSet | Should -Not -BeNullOrEmpty
            $validateSet.ValidValues | Should -Contain 'All'
            $validateSet.ValidValues | Should -Contain 'ARM'
            $validateSet.ValidValues | Should -Contain 'Entra'
        }

        It 'Scope defaults to All' {
            $cmd = Get-Command -Module Devolutions.CIEM -Name Start-CIEMAzureDiscovery
            $scopeParam = $cmd.Parameters['Scope']
            $scopeParam.Attributes | Where-Object { $_ -is [System.Management.Automation.ParameterAttribute] } |
                ForEach-Object { $_.Mandatory | Should -BeFalse }
            $funcDef = (Get-Command -Module Devolutions.CIEM -Name Start-CIEMAzureDiscovery).ScriptBlock.ToString()
            $funcDef | Should -Match '\$Scope\s*=\s*''All'''
        }

        It 'OutputType is CIEMAzureDiscoveryRun' {
            $cmd = Get-Command -Module Devolutions.CIEM -Name Start-CIEMAzureDiscovery
            $outputTypes = $cmd.OutputType.Name
            $outputTypes | Should -Contain 'CIEMAzureDiscoveryRun'
        }
    }

    Context 'Concurrency guard' {
        BeforeEach {
            InModuleScope Devolutions.CIEM {
                # Fake an established auth context so the function reaches the concurrency guard
                $script:AzureAuthContext = [PSCustomObject]@{ IsConnected = $true }
                $script:existingRun = New-CIEMAzureDiscoveryRun -Scope 'All' -Status 'Running' -StartedAt (Get-Date).ToString('o')
            }
        }

        AfterEach {
            InModuleScope Devolutions.CIEM {
                Remove-CIEMAzureDiscoveryRun -Id $script:existingRun.Id -Confirm:$false
            }
        }

        It 'Throws if a Running discovery run already exists' {
            InModuleScope Devolutions.CIEM {
                { Start-CIEMAzureDiscovery } | Should -Throw '*already in progress*'
            }
        }
    }

    Context 'Phase array refactor' {
        BeforeAll {
            $script:StartDiscoverySource = Get-Content (Join-Path $PSScriptRoot '..' '..' 'Public' 'Start-CIEMAzureDiscovery.ps1') -Raw
        }

        It 'Invoke-CIEMDiscoveryPhase generic runner exists (private, no dash)' {
            InModuleScope Devolutions.CIEM {
                Get-Command -Name 'InvokeCIEMDiscoveryPhase' -ErrorAction Stop | Should -Not -BeNullOrEmpty
            }
        }

        It 'ResolveCIEMResourceTypeMetadata lookup helper exists' {
            InModuleScope Devolutions.CIEM {
                Get-Command -Name 'ResolveCIEMResourceTypeMetadata' -ErrorAction Stop | Should -Not -BeNullOrEmpty
            }
        }

        It 'ResolveCIEMResourceTypeMetadata returns ResourceGraph + ResourceContainers for microsoft.resources/* types' {
            InModuleScope Devolutions.CIEM {
                $result = ResolveCIEMResourceTypeMetadata -Type 'microsoft.resources/subscriptions'
                $result.ApiSource | Should -Be 'ResourceGraph'
                $result.GraphTable | Should -Be 'ResourceContainers'
            }
        }

        It 'ResolveCIEMResourceTypeMetadata returns ResourceGraph + AuthorizationResources for microsoft.authorization/* types' {
            InModuleScope Devolutions.CIEM {
                $result = ResolveCIEMResourceTypeMetadata -Type 'microsoft.authorization/roleassignments'
                $result.ApiSource | Should -Be 'ResourceGraph'
                $result.GraphTable | Should -Be 'AuthorizationResources'
            }
        }

        It 'ResolveCIEMResourceTypeMetadata returns ResourceGraph + Resources for generic microsoft.* types' {
            InModuleScope Devolutions.CIEM {
                $result = ResolveCIEMResourceTypeMetadata -Type 'microsoft.compute/virtualmachines'
                $result.ApiSource | Should -Be 'ResourceGraph'
                $result.GraphTable | Should -Be 'Resources'
            }
        }

        It 'ResolveCIEMResourceTypeMetadata returns Graph + null GraphTable for non-microsoft types' {
            InModuleScope Devolutions.CIEM {
                $result = ResolveCIEMResourceTypeMetadata -Type 'user'
                $result.ApiSource | Should -Be 'Graph'
                $result.GraphTable | Should -BeNullOrEmpty
            }
        }

        It 'Start-CIEMAzureDiscovery source file no longer contains the monolithic if/elif Save-ResourceTypes chain' {
            # The old helper had a 3-arm if/elseif chain matching resource type prefixes.
            # After refactor, the chain should be replaced with a single hashtable lookup.
            # Count the chained elseif+match clauses that reference microsoft.* prefixes.
            $chainCount = ([regex]::Matches($script:StartDiscoverySource, "-match\s+'\^microsoft\\\.")).Count
            $chainCount | Should -BeLessThan 3
        }

        It 'Start-CIEMAzureDiscovery defines a phase array driving Invoke-CIEMDiscoveryPhase' {
            # After refactor the function body should invoke Invoke-CIEMDiscoveryPhase
            # at least once (ideally multiple times in a foreach loop).
            $script:StartDiscoverySource | Should -Match 'InvokeCIEMDiscoveryPhase'
        }
    }

    Context 'Private collection helpers exist' {
        It 'InvokeCIEMResourceGraphQuery exists (private, no dash)' {
            InModuleScope Devolutions.CIEM {
                Get-Command InvokeCIEMResourceGraphQuery -ErrorAction Stop
            } | Should -Not -BeNullOrEmpty
        }

        It 'GetCIEMBuiltInRoleDefinitions exists (private)' {
            InModuleScope Devolutions.CIEM {
                Get-Command GetCIEMBuiltInRoleDefinitions -ErrorAction Stop
            } | Should -Not -BeNullOrEmpty
        }

        It 'InvokeCIEMEntraEntityCollection exists (private)' {
            InModuleScope Devolutions.CIEM {
                Get-Command InvokeCIEMEntraEntityCollection -ErrorAction Stop
            } | Should -Not -BeNullOrEmpty
        }

        It 'InvokeCIEMEntraPermissionCollection exists (private)' {
            InModuleScope Devolutions.CIEM {
                Get-Command InvokeCIEMEntraPermissionCollection -ErrorAction Stop
            } | Should -Not -BeNullOrEmpty
        }

        It 'InvokeCIEMEntraRelationshipCollection exists (private)' {
            InModuleScope Devolutions.CIEM {
                Get-Command InvokeCIEMEntraRelationshipCollection -ErrorAction Stop
            } | Should -Not -BeNullOrEmpty
        }

        It 'InvokeCIEMTransaction exists (private)' {
            InModuleScope Devolutions.CIEM {
                Get-Command InvokeCIEMTransaction -ErrorAction Stop
            } | Should -Not -BeNullOrEmpty
        }

        It 'InvokeCIEMAzureEffectiveRoleAssignmentBuild exists (private)' {
            InModuleScope Devolutions.CIEM {
                Get-Command InvokeCIEMAzureEffectiveRoleAssignmentBuild -ErrorAction Stop
            } | Should -Not -BeNullOrEmpty
        }
    }
}