public/Invoke-MyCorp.ps1

<#
.SYNOPSIS
    Main MyCorp command that runs tests and generates reports.
 
.DESCRIPTION
    Invoke-MyCorp runs Pester tests and generates reports in HTML, Markdown, JSON, CSV and Excel formats.
#>

function Invoke-MyCorp {
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '', Justification = 'Colors are helpful for UX')]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'False positives for ExportCsv/ExportExcel')]
    [CmdletBinding()]
    param (
        [Parameter(Position = 0)]
        [string] $Path,

        [string[]] $Tag,
        [string[]] $ExcludeTag,

        [string] $OutputHtmlFile,
        [string] $OutputMarkdownFile,
        [string] $OutputJsonFile,
        [string] $OutputFolder,
        [string] $OutputFolderFileName,
        [string] $SubscriptionId,

        [PesterConfiguration] $PesterConfiguration,

        [ValidateSet('None','Normal','Detailed','Diagnostic')]
        [string] $Verbosity = 'None',

        [switch] $NonInteractive,
        [switch] $PassThru,
        [string[]] $MailRecipient,
        [string] $MailTestResultsUri,
        [string] $MailUserId,
        [string] $TeamId,
        [string] $TeamChannelId,
        [string] $TeamChannelWebhookUri,
        [switch] $SkipGraphConnect,
        [switch] $DisableTelemetry,
        [switch] $SkipVersionCheck,
        [switch] $ExportCsv,
        [switch] $ExportExcel,
        [switch] $NoLogo,
        [string] $DriftRoot
    )

    # -----------------------
    # Session and helper init
    # -----------------------
    if (-not (Test-Path variable:\__MyCorpSession)) {
        New-Variable -Name __MyCorpSession -Value ([PSCustomObject]@{
            Connections = @()
            MyCorpConfig = $null
        }) -Scope Script -Force | Out-Null
    }

    function GetDefaultFileName {
        $timestamp = Get-Date -Format "yyyy-MM-dd-HHmmss"
        return "TestResults-$timestamp.html"
    }

    function ValidateAndSetOutputFiles {
        param($out)

        $result = $null

        if (-not [string]::IsNullOrEmpty($out.OutputHtmlFile) -and ($out.OutputHtmlFile -notlike '*.html')) {
            $result = 'The OutputHtmlFile parameter must have an .html extension.'
            return $result
        }
        if (-not [string]::IsNullOrEmpty($out.OutputMarkdownFile) -and ($out.OutputMarkdownFile -notlike '*.md')) {
            $result = 'The OutputMarkdownFile parameter must have an .md extension.'
            return $result
        }
        if (-not [string]::IsNullOrEmpty($out.OutputJsonFile) -and ($out.OutputJsonFile -notlike '*.json')) {
            $result = 'The OutputJsonFile parameter must have a .json extension.'
            return $result
        }

        $someOutputFileHasValue = -not [string]::IsNullOrEmpty($out.OutputHtmlFile) -or `
            -not [string]::IsNullOrEmpty($out.OutputMarkdownFile) -or -not [string]::IsNullOrEmpty($out.OutputJsonFile)

        if ([string]::IsNullOrEmpty($out.OutputFolder) -and -not $someOutputFileHasValue) {
            $out.OutputFolder = './test-results'
        }

        if (-not [string]::IsNullOrEmpty($out.OutputFolder)) {
            try { New-Item -Path $out.OutputFolder -ItemType Directory -Force -ErrorAction SilentlyContinue | Out-Null } catch {}
            if ([string]::IsNullOrEmpty($out.OutputFolderFileName)) {
                $out.OutputFolderFileName = "TestResults-$(Get-Date -Format 'yyyy-MM-dd-HHmmss')"
            }

            $out.OutputHtmlFile = Join-Path $out.OutputFolder "$($out.OutputFolderFileName).html"
            $out.OutputMarkdownFile = Join-Path $out.OutputFolder "$($out.OutputFolderFileName).md"
            $out.OutputJsonFile = Join-Path $out.OutputFolder "$($out.OutputFolderFileName).json"

            if ($ExportCsv.IsPresent) { $out.OutputCsvFile = Join-Path $out.OutputFolder "$($out.OutputFolderFileName).csv" }
            if ($ExportExcel.IsPresent) { $out.OutputExcelFile = Join-Path $out.OutputFolder "$($out.OutputFolderFileName).xlsx" }
        }

        return $result
    }

    function GetPesterConfiguration {
        param($Path, $Tag, $ExcludeTag, $PesterConfiguration)
        if (-not $PesterConfiguration) { $PesterConfiguration = New-PesterConfiguration }
        $PesterConfiguration.Run.PassThru = $true
        $PesterConfiguration.Output.Verbosity = $Verbosity

        if ($Path) { $PesterConfiguration.Run.Path = $Path } else {
            if (Test-Path -Path "./powershell/tests/pester.ps1") { $PesterConfiguration.Run.Path = './tests' }
            else { $PesterConfiguration.Run.Path = './' }
        }
        if ($Tag) { $PesterConfiguration.Filter.Tag = $Tag }
        if ($ExcludeTag) { $PesterConfiguration.Filter.ExcludeTag = $ExcludeTag }

        return $PesterConfiguration
    }

    # -----------------------
    # Version, banner and telemetry
    # -----------------------
    $version = $null
    try { $version = (Get-MtModuleVersion) } catch { $version = 'Unknown' }

    if ($NonInteractive.IsPresent -or $NoLogo.IsPresent) {
        Write-Verbose "Running MyCorp v$version"
    } else {
        $banner = @'
$$\ $$\ $$$$$$\
$$$\ $$$ | $$ __$$\
$$$$\ $$$$ |$$\ $$\ $$ / \__| $$$$$$\ $$$$$$\ $$$$$$\
$$\$$\$$ $$ |$$ | $$ |$$ | $$ __$$\ $$ __$$\ $$ __$$\
$$ \$$$ $$ |$$ | $$ |$$ | $$ / $$ |$$ | \__|$$ / $$ |
$$ |\$ /$$ |$$ | $$ |$$ | $$\ $$ | $$ |$$ | $$ | $$ |
$$ | \_/ $$ |\$$$$$$$ |\$$$$$$ |\$$$$$$ |$$ | $$$$$$$ |
\__| \__| \____$$ | \______/ \______/ \__| $$ ____/
             $$\ $$ | $$ |
             \$$$$$$ | $$ |
              \______/ \__|
'@

        Write-Host $banner -ForegroundColor Red
        Write-Host "MyCorp Test Runner v$version" -ForegroundColor Red
    }

    # Telemetry: try safe event names and swallow failures (validation may reject unknown names)
    if (-not $DisableTelemetry) {
        try {
            Write-Telemetry -EventName 'InvokeMyCorp'
        } catch {
            try { Write-Telemetry -EventName 'InvokeMaester' } catch { }
        }
    }

    # -----------------------
    # Graph connection check
    # -----------------------
    $isMail = $null -ne $MailRecipient
    $isTeamsChannelMessage = -not ([String]::IsNullOrEmpty($TeamId) -or [String]::IsNullOrEmpty($TeamChannelId))

    if ($SkipGraphConnect) {
        if (-not $NonInteractive.IsPresent) { Write-Host "🔥 Skipping graph connection check" -ForegroundColor Yellow }
    } else {
        try {
            $ok = Test-MtContext -SendMail:$isMail -SendTeamsMessage:$isTeamsChannelMessage
            if (-not $ok) { return }
        } catch {
            Write-Error $_
            return
        }
    }

    # Initialize session - prefer new name but fall back to Mt*
    try {
        if (Get-Command -Name Initialize-MyCorpSession -ErrorAction SilentlyContinue) {
            Initialize-MyCorpSession
        } elseif (Get-Command -Name Initialize-MtSession -ErrorAction SilentlyContinue) {
            Initialize-MtSession
        }
    } catch {
        Write-Verbose "Warning: session initialization failed or not found: $_"
    }
    # ---------------------------------------------------------
    # Azure Subscription Discovery for Subscription-Wise Testing
    # ---------------------------------------------------------
    try {
        $subs = Get-AzSubscription -ErrorAction Stop | Select-Object Id, Name, TenantId, State
        $__MyCorpSession.Subscriptions = $subs

        if ($subs.Count -eq 0) {
            Write-Verbose 'No Azure subscriptions found.'
        }
        elseif ($subs.Count -eq 1) {
            $__MyCorpSession.DefaultSubscription = $subs[0]
            $__MyCorpSession.SelectedSubscription = $subs[0]
            Set-AzContext -SubscriptionId $subs[0].Id -ErrorAction SilentlyContinue
            Write-Verbose "Using single subscription: $($subs[0].Name) ($($subs[0].Id))"
        }
        else {
            Write-Verbose "Discovered $($subs.Count) Azure subscriptions."
            Write-Host "Available Azure Subscriptions:" -ForegroundColor Cyan

            $i = 0
            foreach ($s in $subs) {
                $i++
                Write-Host ("[{0}] {1} ({2})" -f $i, $s.Name, $s.Id)
            }

            Write-Host "`nCall Select-MyCorpSubscription -Index <number> to choose a subscription." -ForegroundColor Yellow
        }
    }
    catch {
        Write-Warning "Failed to enumerate Azure subscriptions: $($_.Exception.Message)"
        $__MyCorpSession.Subscriptions = @()
    }

        # Validate webhook url if provided
        if (-not [string]::IsNullOrEmpty($TeamChannelWebhookUri)) {
            $urlPattern = '^(https)://[^\s/$.?#].[^\s]*$'
            if (-not ($TeamChannelWebhookUri -match $urlPattern)) {
                Write-Error -Message "Invalid Webhook URL: $TeamChannelWebhookUri"
                return
            }
        }

    # -----------------------
    # Output file handling
    # -----------------------
    $out = [PSCustomObject]@{
        OutputFolder = $OutputFolder
        OutputFolderFileName = $OutputFolderFileName
        OutputHtmlFile = $OutputHtmlFile
        OutputMarkdownFile = $OutputMarkdownFile
        OutputJsonFile = $OutputJsonFile
        OutputCsvFile = $null
        OutputExcelFile = $null
    }

    $result = ValidateAndSetOutputFiles $out
    if ($result) { Write-Error -Message $result; return }

    # -----------------------
    # Tags defaults and compatibility
    # -----------------------
    if (-not $Tag) { $Tag = @() }
    if (-not $ExcludeTag) { $ExcludeTag = @() }

    # Backwards-compat defaults
    if ("CAWhatIf" -notin $Tag) { $ExcludeTag += 'CAWhatIf' }
    if (-not $Tag -or $Tag.Count -eq 0) { $ExcludeTag += 'Full' }
    if ('Full' -in $Tag) { $Tag += 'All' }

    # -----------------------
    # Path validation
    # -----------------------
    $pesterConfig = GetPesterConfiguration -Path $Path -Tag $Tag -ExcludeTag $ExcludeTag -PesterConfiguration $PesterConfiguration
    $Path = $pesterConfig.Run.Path.value
    Write-Verbose "Merged configuration: $($pesterConfig | ConvertTo-Json -Depth 5 -Compress)"

    if (Test-Path -Path $Path -PathType Leaf) {
        if ($NonInteractive.IsPresent) { Write-Error -Message "The path '$Path' is a file. Provide a folder." } else {
            Write-Host "The path '$Path' is a file. Please provide a folder path." -ForegroundColor Red
            Write-Host "→ Update-MyCorpTests" -ForegroundColor Green
        }
        return
    }

    if (-not (Test-Path -Path $Path -PathType Container)) {
        if ($NonInteractive.IsPresent) { Write-Error -Message "The path '$Path' does not exist." } else {
            Write-Host "The path '$Path' does not exist." -ForegroundColor Red
            Write-Host "→ Update-MyCorpTests" -ForegroundColor Green
        }
        return
    }

    # Check for test files, handle Get-ChildItem errors silently and test by Count
    try {
        $testFiles = Get-ChildItem -Path (Join-Path -Path $Path -ChildPath '*.Tests.ps1') -Recurse -ErrorAction SilentlyContinue
    } catch {
        $testFiles = @()
    }
    if (-not $testFiles -or $testFiles.Count -eq 0) {
        if ($NonInteractive.IsPresent) { Write-Error -Message "No test files found in the path '$Path'." } else {
            Write-Host "No test files found in the path '$Path'." -ForegroundColor Red
            Write-Host "→ Update-MyCorpTests" -ForegroundColor Green
        }
        return
    }

    # -----------------------
    # Drift root handling
    # -----------------------
    if ($DriftRoot) {
        try {
            $resolved = (Resolve-Path -Path $DriftRoot -ErrorAction SilentlyContinue).Path
            if (-not (Test-Path -Path $resolved)) {
                Write-Warning "The specified drift root directory '$DriftRoot' does not exist."
            } else {
                Set-Item -Path Env:\MyCorp_FOLDER_DRIFT -Value $resolved -ErrorAction SilentlyContinue
                Write-Verbose "Drift root directory set to: $resolved"
            }
        } catch {
            Write-Verbose "Drift root handling failed: $_"
        }
    }

    # -----------------------
    # Run tests
    # -----------------------
    Set-MtProgressView
    Write-MtProgress -Activity "Starting MyCorp" -Status "Reading MyCorp config..." -Force

    try {
        $__MyCorpSession.MyCorpConfig = Get-MtMyCorpConfig -Path $Path -ErrorAction SilentlyContinue
    } catch {
        Write-Verbose "Failed to read configuration via Get-MtMyCorpConfig: $_"
    }

    Write-MtProgress -Activity "Starting MyCorp" -Status "Discovering tests to run..." -Force

    $pesterResults = $null
    try {
        $pesterResults = Invoke-Pester -Configuration $pesterConfig
    } catch {
        Write-Error "Invoke-Pester failed: $_"
        Reset-MtProgressView
        return
    }

    if ($pesterResults) {
        Write-MtProgress -Activity "Processing test results" -Status "$($pesterResults.TotalCount) test(s)" -Force

        # Convert/present results
        try {
            $mycorpResults = ConvertTo-MtMyCorpResult -PesterResults $pesterResults -ErrorAction SilentlyContinue
        } catch {
            Write-Verbose "ConvertTo-MtMyCorpResult failed: $_"
            $mycorpResults = $null
        }

        if ($mycorpResults -and -not [string]::IsNullOrEmpty($out.OutputJsonFile)) {
            try { $mycorpResults | ConvertTo-Json -Depth 5 -WarningAction SilentlyContinue | Out-File -FilePath $out.OutputJsonFile -Encoding UTF8 } catch {}
        }

        if ($mycorpResults -and -not [string]::IsNullOrEmpty($out.OutputMarkdownFile)) {
            try {
                Write-MtProgress -Activity "Creating markdown report"
                $output = Get-MtMarkdownReport -MtMyCorpResults $mycorpResults -ErrorAction SilentlyContinue
                $output | Out-File -FilePath $out.OutputMarkdownFile -Encoding UTF8
            } catch {}
        }

        if ($mycorpResults -and -not [string]::IsNullOrEmpty($out.OutputCsvFile)) {
            try {
                Write-MtProgress -Activity "Creating CSV"
                Convert-MtResultsToFlatObject -InputObject $mycorpResults -CsvFilePath $out.OutputCsvFile -ErrorAction SilentlyContinue
            } catch {}
        }

        if ($mycorpResults -and -not [string]::IsNullOrEmpty($out.OutputExcelFile)) {
            try {
                Write-MtProgress -Activity "Creating Excel workbook"
                Convert-MtResultsToFlatObject -InputObject $mycorpResults -ExcelFilePath $out.OutputExcelFile -ErrorAction SilentlyContinue
            } catch {}
        }

        if ($mycorpResults -and -not [string]::IsNullOrEmpty($out.OutputHtmlFile)) {
            try {
                Write-MtProgress -Activity "Creating html report"
                $output = Get-MtHtmlReport -MtMyCorpResults $mycorpResults -ErrorAction SilentlyContinue
                $output | Out-File -FilePath $out.OutputHtmlFile -Encoding UTF8
                if (-not $NonInteractive.IsPresent) { Write-Host "MyCorp test report generated at $($out.OutputHtmlFile)" -ForegroundColor Green }
                if ((Get-MtUserInteractive) -and (-not $NonInteractive)) { Invoke-Item $out.OutputHtmlFile | Out-Null }
            } catch {}
        }

        # Notifications
        if ($mycorpResults -and $MailRecipient) {
            try {
                Write-MtProgress -Activity "Sending mail"
                Send-MtMail -MtMyCorpResults $mycorpResults -Recipient $MailRecipient -TestResultsUri $MailTestResultsUri -UserId $MailUserId -ErrorAction SilentlyContinue
            } catch {}
        }

        if ($mycorpResults -and $TeamId -and $TeamChannelId) {
            try {
                Write-MtProgress -Activity "Sending Teams message"
                Send-MtTeamsMessage -MtMyCorpResults $mycorpResults -TeamId $TeamId -TeamChannelId $TeamChannelId -TestResultsUri $MailTestResultsUri -ErrorAction SilentlyContinue
            } catch {}
        }

        if ($mycorpResults -and $TeamChannelWebhookUri) {
            try {
                Write-MtProgress -Activity "Sending Teams message"
                Send-MtTeamsMessage -MtMyCorpResults $mycorpResults -TeamChannelWebhookUri $TeamChannelWebhookUri -TestResultsUri $MailTestResultsUri -ErrorAction SilentlyContinue
            } catch {}
        }

        if ($Verbosity -eq 'None') {
            Write-Host "`nTests Passed: $($pesterResults.PassedCount), Failed: $($pesterResults.FailedCount), Skipped: $($pesterResults.SkippedCount)`n"
        }

        if (-not $SkipVersionCheck -and $version -and $version -ne 'Next') {
            try { Get-IsNewMyCorpVersionAvailable | Out-Null } catch {}
        }

        Write-MtProgress -Activity "Completed tests" -Status "Total $($pesterResults.TotalCount)" -Completed -Force
    }

    Reset-MtProgressView

    if ($PassThru) { return $mycorpResults }
}
# End function Invoke-MyCorp