tests/Test-EntraSourceSetup.ps1

param(
    [string]$ModuleRoot = (Join-Path (Split-Path -Parent $PSScriptRoot) 'modules')
)

. (Join-Path $PSScriptRoot '_TestHelpers.ps1')
$script:AssertionCount = 0

Import-IscModule -Path (Join-Path $ModuleRoot 'ISC.EntraSourceSetup.psm1') -Force
Initialize-EntraSourceSetup -ModuleRoot $ModuleRoot

$catalog = Get-EntraAgentCatalog
Assert-True ($catalog.featurePacks.Count -gt 0) 'catalog exposes feature packs'
Assert-True ($catalog.directoryRoles -contains 'UserAdministrator') 'catalog exposes directory roles'
$entraPacks = (Get-EntraConnectorCatalog).FeaturePacks
$entraLabels = @($entraPacks.Keys | ForEach-Object { [string]$entraPacks[$_].Label })
Assert-Equal (($entraLabels | Sort-Object) -join '|') ($entraLabels -join '|') 'agent catalog feature packs are ordered by label'
Assert-True ($catalog.secretFields -contains 'agent365RefreshToken') 'catalog accepts a supplied Agent 365 refresh token'

$checklist = @(Get-EntraIscFeatureChecklist -FeatureNames @('Agent365'))
Assert-True ([bool]($checklist | Where-Object { $_ -like '*Machine Identity Governance*refresh token*' })) 'checklist points at Machine Identity Governance Settings'

# The connector default filter is servicePrincipalType eq 'Application', which silently drops every
# Foundry and Copilot Studio agent identity because Entra types those as ServiceIdentity.
$spnChecklist = @(Get-EntraIscFeatureChecklist -FeatureNames @('ServicePrincipalProvisioning'))
Assert-True ([bool]($spnChecklist | Where-Object { $_ -like '*ServiceIdentity*' })) 'checklist warns that the default SPN filter hides agent identities'
$copilotChecklist = @(Get-EntraIscFeatureChecklist -FeatureNames @('CopilotDiscovery'))
Assert-True ([bool]($copilotChecklist | Where-Object { $_ -like '*Cognitive Services Data Contributor*' })) 'checklist names both Azure roles Foundry discovery needs'
Assert-True ([bool]($copilotChecklist | Where-Object { $_ -like '*user_impersonation*' })) 'checklist names the Azure Service Management delegated permission'
Assert-True ([bool]($copilotChecklist | Where-Object { $_ -like '*Dataverse application user*' })) 'checklist says Copilot Studio needs a Dataverse application user'
Assert-True ([bool]($copilotChecklist | Where-Object { $_ -like '*Global Discovery Service Role*' })) 'checklist names the Power Platform roles Copilot Studio needs'

# Selecting the Copilot Studio pack has to plan the Dataverse work, because Entra permissions alone
# never reach a Power Platform environment.
Assert-True (Test-CopilotStudioSelected -FeatureNames @('CopilotDiscovery')) 'the Copilot Studio pack is recognised'
Assert-True (-not (Test-CopilotStudioSelected -FeatureNames @('Agent365'))) 'other packs do not trigger the Dataverse step'
$copilotPlan = New-EntraAgentPlan -Request @{ config = @{ applicationName = ''; features = @('CopilotDiscovery') } }
Assert-True (@($copilotPlan.mutations) -contains 'configure-copilot-studio-dataverse-access') 'the plan includes the Dataverse application user step'
Assert-True ([bool](@($copilotPlan.manualSteps) | Where-Object { $_ -like '*az login*' })) 'the plan warns that the Azure CLI must be signed in'
Assert-True (@($catalog.optionalConfig) -contains 'powerPlatformEnvironmentUrl') 'the environment URL can be supplied explicitly'

$agent365Request = [ordered]@{
    schemaVersion = 1
    connector     = 'entra-id'
    config        = [ordered]@{ applicationName = 'SailPoint ISC Entra ID'; features = @('Agent365') }
    secretRefs    = [ordered]@{ agent365RefreshToken = 'env:AGENT365_REFRESH_TOKEN' }
}
$agent365Resolved = Get-EntraResolvedConfig -Request $agent365Request
Assert-True (Test-Agent365Selected -FeatureNames $agent365Resolved.Feature) 'Agent365 feature is recognised'
Assert-Equal 'env:AGENT365_REFRESH_TOKEN' $agent365Resolved.Agent365RefreshTokenRef 'refresh token reference is carried through'
Assert-Equal 'http://localhost:8400/' $agent365Resolved.Agent365RedirectUri 'redirect URI falls back to the loopback default'

$request = [ordered]@{
    schemaVersion = 1
    connector     = 'entra-id'
    config        = [ordered]@{
        applicationName = 'SailPoint ISC Entra ID'
        directoryRole   = 'UserAdministrator'
        features        = @('MfaManagement')
    }
    decisions = [ordered]@{ createNewApplication = $true; createSecret = $false }
}

$plan = New-EntraAgentPlan -Request $request
Assert-True ($plan.status -in @('ready', 'needsInput')) 'plan returns structured status'
Assert-True ($plan.requestHash.Length -eq 64) 'plan carries request hash'

# The refresh token is not a Connection Settings value, so the result has to keep it out of the
# public settings while still handing it to the operator. The agent adapter is deliberately not
# imported here: the wizard does not load it either, so writing secrets has to work without it.
$outputDirectory = Join-Path ([System.IO.Path]::GetTempPath()) ('entra-test-' + [guid]::NewGuid().ToString('N'))
try {
    $agent365Resolved.OutputDirectory = $outputDirectory
    $applyResult = [PSCustomObject]@{
        Session              = [PSCustomObject]@{ TenantId = 'tid'; DomainName = 'contoso.onmicrosoft.com' }
        IsUpdate             = $false
        Permissions          = @()
        DelegatedPermissions = @()
        RoleNames            = @()
        ConsentFailures      = @()
        Pair                 = [PSCustomObject]@{ Application = [PSCustomObject]@{ AppId = 'app-id'; Id = 'obj' }; ServicePrincipal = [PSCustomObject]@{ Id = 'sp' } }
        SecretValue          = $null
        SecretExpires        = $null
        AssignedRoles        = @()
        Agent365RefreshToken = 'refresh-token-value'
        Agent365Pending      = $null
    }

    $result = Build-EntraSourceResult -Resolved $agent365Resolved -ApplyResult $applyResult -RunDirectory $outputDirectory
    Assert-True (-not (@($result.connectionSettings.Keys) -contains 'Agent 365 Refresh Token')) 'refresh token stays out of the public connection settings'
    Assert-Equal 1 (@($result.secretArtifacts | Where-Object { $_.label -eq 'Agent 365 Refresh Token' }).Count) 'refresh token is saved as a restricted artifact'
    Assert-True ([bool]($result.completionItems | Where-Object { $_.Label -like 'Agent 365 Refresh Token*' -and $_.Mask })) 'refresh token is offered masked'
    Assert-True ([bool]($result.situation | Where-Object { $_ -like '*Machine Identity Governance Settings*' })) 'result says where the refresh token belongs'

    $applyResult.Agent365RefreshToken = $null
    $applyResult.Agent365Pending = 'no client secret was available to mint it'
    $pendingResult = Build-EntraSourceResult -Resolved $agent365Resolved -ApplyResult $applyResult -RunDirectory $outputDirectory
    Assert-True ([bool]($pendingResult.situation | Where-Object { $_ -like 'Pending: the Agent 365 refresh token*' })) 'a missing refresh token is reported as pending'
    Assert-Equal 0 (@($pendingResult.completionItems | Where-Object { $_.Label -like 'Agent 365*' }).Count) 'nothing is offered to copy when no token was issued'

    # Writing secrets used to fail only from the wizard, because the plan path happens to load the
    # agent adapter first and hides the missing dependency. A child process with the wizard's own
    # imports is the only way to reproduce that.
    $wizardScript = Join-Path $outputDirectory 'wizard-imports.ps1'
    Set-Content -LiteralPath $wizardScript -Encoding UTF8 -Value @'
param([string]$ModuleRoot, [string]$RunDirectory)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
Import-Module (Join-Path $ModuleRoot 'ISC.OperatorConsole.psm1') -Force -WarningAction SilentlyContinue
Import-IscModule -Path (Join-Path $ModuleRoot 'ISC.OperatorToolchain.psm1') -Force
Import-IscModule -Path (Join-Path $ModuleRoot 'ISC.EntraConnector.psm1') -Force
Import-IscModule -Path (Join-Path $ModuleRoot 'ISC.EntraCiemConnector.psm1') -Force
Import-IscModule -Path (Join-Path $ModuleRoot 'ISC.EntraSourceSetup.psm1') -Force
Initialize-OperatorConsole -NonInteractive
Initialize-EntraSourceSetup -ModuleRoot $ModuleRoot -NonInteractive

$resolved = [PSCustomObject]@{ Feature = @('Agent365'); OutputDirectory = $RunDirectory }
$applyResult = [PSCustomObject]@{
    Session = [PSCustomObject]@{ TenantId = 'tid'; DomainName = 'contoso.onmicrosoft.com' }
    ConsentFailures = @()
    RoleNames = @()
    AssignedRoles = @()
    Pair = [PSCustomObject]@{ Application = [PSCustomObject]@{ AppId = 'app-id'; Id = 'obj' } }
    SecretValue = 'client-secret-value'
    Agent365RefreshToken = 'refresh-token-value'
    Agent365Pending = $null
}
$null = Build-EntraSourceResult -Resolved $resolved -ApplyResult $applyResult -RunDirectory $RunDirectory
foreach ($name in @('client-secret.txt', 'agent365-refresh-token.txt')) {
    if (-not (Test-Path -LiteralPath (Join-Path $RunDirectory $name))) { throw "$name was not written" }
}
'@

    $wizardRun = Join-Path $outputDirectory 'wizard-run'
    & pwsh -NoProfile -File $wizardScript -ModuleRoot $ModuleRoot -RunDirectory $wizardRun 2>&1 | Out-String | Write-Verbose
    Assert-Equal 0 $LASTEXITCODE 'the wizard module set can write secret artifacts without the agent adapter'
}
finally {
    if (Test-Path -LiteralPath $outputDirectory) {
        Remove-Item -LiteralPath $outputDirectory -Recurse -Force
    }
}

Write-Host "PASS ($script:AssertionCount assertions)"