Run-Tests.ps1

<#
.SYNOPSIS
    Loads local Dev module builds and runs the Pester test suite.

.DESCRIPTION
    Imports Dev.Logic.Monitor.psd1 from this repository and Dev.Logic.Monitor.SE.psd1
    from the sibling Logic.Monitor.SE repository, then runs Invoke-Pester.

    Credentials are resolved in order:
      1. Explicit parameters
      2. LM_* environment variables (same names as GitHub Actions)
      3. Cached credentials from the Logic.Monitor secret vault (-CachedAccountName)

.PARAMETER Path
    Test file or directory to run. Defaults to ./Tests/

.PARAMETER SkipBuild
    Skip running ./Build.ps1 before loading modules.

.PARAMETER SEModulePath
    Path to the Logic.Monitor.SE repository. Defaults to ../Logic.Monitor.SE

.PARAMETER CachedAccountName
    Name of a cached Logic.Monitor vault entry to use for test credentials.

.PARAMETER AccessId
    LogicMonitor API access ID. Falls back to $env:LM_ACCESS_ID.

.PARAMETER AccessKey
    LogicMonitor API access key. Falls back to $env:LM_ACCESS_KEY.

.PARAMETER AccountName
    LogicMonitor portal subdomain. Falls back to $env:LM_PORTAL.

.PARAMETER BearerToken
    LogicMonitor bearer token. Falls back to $env:LM_BEARER_TOKEN.

.PARAMETER PreferredCollectorId
    Collector ID used by device and uptime tests. Defaults to 76.

.PARAMETER PassThru
    Return the Pester result object.

.EXAMPLE
    ./Run-Tests.ps1

.EXAMPLE
    ./Run-Tests.ps1 -CachedAccountName "dev-portal"

.EXAMPLE
    ./Run-Tests.ps1 -Path ./Tests/LMDevice.Tests.ps1 -SkipBuild
#>


[CmdletBinding()]
param(
    [string]$Path = (Join-Path $PSScriptRoot 'Tests'),
    [switch]$SkipBuild,
    [string]$SEModulePath = (Join-Path $PSScriptRoot '..' 'Logic.Monitor.SE'),
    [string]$CachedAccountName,
    [string]$AccessId,
    [string]$AccessKey,
    [string]$AccountName,
    [string]$BearerToken,
    [string]$PreferredCollectorId = '76',
    [switch]$PassThru
)

$ErrorActionPreference = 'Stop'

function Resolve-LMDevModulePath {
    param(
        [Parameter(Mandatory)]
        [string]$SearchPath,

        [string]$Label
    )

    $resolvedPath = Resolve-Path -Path $SearchPath -ErrorAction Stop
    $devModule = Get-ChildItem -Path $resolvedPath -Filter 'Dev.*.psd1' -File | Select-Object -First 1

    if (-not $devModule) {
        throw "No Dev module manifest (Dev.*.psd1) found in $resolvedPath for $Label."
    }

    return $devModule.FullName
}

function Resolve-LMTestCredentials {
    param(
        [string]$CachedAccountName,
        [string]$AccessId,
        [string]$AccessKey,
        [string]$AccountName,
        [string]$BearerToken
    )

    if ($AccessId -and $AccessKey -and $AccountName) {
        return [ordered]@{
            AccessId          = $AccessId
            AccessKey           = $AccessKey
            AccountName         = $AccountName
            BearerToken         = $BearerToken
            AuthType            = 'LMv1'
            CachedAccountName   = $null
            GovCloud            = $false
        }
    }

    if ($BearerToken -and $AccountName -and -not $AccessKey) {
        return [ordered]@{
            AccessId          = $AccessId
            AccessKey           = $null
            AccountName         = $AccountName
            BearerToken         = $BearerToken
            AuthType            = 'Bearer'
            CachedAccountName   = $null
            GovCloud            = $false
        }
    }

    if ($env:LM_ACCESS_ID -and $env:LM_ACCESS_KEY -and $env:LM_PORTAL) {
        return [ordered]@{
            AccessId          = $env:LM_ACCESS_ID
            AccessKey           = $env:LM_ACCESS_KEY
            AccountName         = $env:LM_PORTAL
            BearerToken         = if ($BearerToken) { $BearerToken } else { $env:LM_BEARER_TOKEN }
            AuthType            = 'LMv1'
            CachedAccountName   = $null
            GovCloud            = $false
        }
    }

    if (-not $CachedAccountName) {
        Write-Warning @"
No test credentials were provided. Integration tests that call Connect-LMAccount will fail.
Supply -AccessId/-AccessKey/-AccountName, set LM_ACCESS_ID/LM_ACCESS_KEY/LM_PORTAL, or use -CachedAccountName.
"@

        return [ordered]@{
            AccessId          = $null
            AccessKey           = $null
            AccountName         = $null
            BearerToken         = $BearerToken
            AuthType            = $null
            CachedAccountName   = $null
            GovCloud            = $false
        }
    }

    try {
        Get-SecretVault -Name Logic.Monitor -ErrorAction Stop | Out-Null
    }
    catch {
        throw "Cached account '$CachedAccountName' was requested but the Logic.Monitor secret vault is not configured."
    }

    $cachedAccount = Get-SecretInfo -Vault Logic.Monitor -Name $CachedAccountName -ErrorAction Stop
    if (-not $cachedAccount) {
        throw "Cached account '$CachedAccountName' was not found in the Logic.Monitor vault."
    }

    $credentialType = if ($cachedAccount.Metadata['Type']) { $cachedAccount.Metadata['Type'] } else { 'LMv1' }
    $secretValue = Get-Secret -Vault Logic.Monitor -Name $CachedAccountName -AsPlainText -ErrorAction Stop

    $resolved = [ordered]@{
        AccessId          = $cachedAccount.Metadata['Id']
        AccountName       = $cachedAccount.Metadata['Portal']
        BearerToken       = $null
        AccessKey           = $null
        AuthType            = $credentialType
        CachedAccountName   = $CachedAccountName
        GovCloud            = ($cachedAccount.Metadata['GovCloud'] -eq 'True')
    }

    if ($credentialType -eq 'Bearer') {
        $resolved.BearerToken = $secretValue
    }
    else {
        $resolved.AccessKey = $secretValue
    }

    if (-not $resolved.AccountName) {
        throw "Cached account '$CachedAccountName' is missing portal metadata."
    }

    if ($credentialType -eq 'LMv1' -and (-not $resolved.AccessId -or -not $resolved.AccessKey)) {
        throw "Cached account '$CachedAccountName' is missing LMv1 credential metadata."
    }

    return $resolved
}

if (-not (Get-Module -ListAvailable -Name Pester | Where-Object { $_.Version.Major -ge 5 })) {
    throw 'Pester 5.x is required. Install it with: Install-Module Pester -MinimumVersion 5.0.0 -Force'
}

if (-not $SkipBuild) {
    if (-not $env:BUILD_VERSION) {
        $env:BUILD_VERSION = '9.9.9'
    }

    Write-Host "Building module (BUILD_VERSION=$($env:BUILD_VERSION))..." -ForegroundColor Cyan
    & (Join-Path $PSScriptRoot 'Build.ps1')
}

$devModulePath = Resolve-LMDevModulePath -SearchPath $PSScriptRoot -Label 'Logic.Monitor'
$seDevModulePath = Resolve-LMDevModulePath -SearchPath $SEModulePath -Label 'Logic.Monitor.SE'

Write-Host "Importing $([IO.Path]::GetFileName($devModulePath))..." -ForegroundColor Cyan
Import-Module $devModulePath -Force

Write-Host "Importing $([IO.Path]::GetFileName($seDevModulePath))..." -ForegroundColor Cyan
Import-Module $seDevModulePath -Force

$credentials = Resolve-LMTestCredentials @PSBoundParameters

$pesterData = @{
    Module               = $devModulePath
    ModuleName           = [IO.Path]::GetFileNameWithoutExtension($devModulePath)
    PreferredCollectorId = $PreferredCollectorId
}

foreach ($key in @('AccessId', 'AccessKey', 'AccountName', 'BearerToken', 'CachedAccountName', 'AuthType', 'GovCloud')) {
    if ($null -ne $credentials[$key] -and $credentials[$key] -ne '') {
        $pesterData[$key] = $credentials[$key]
    }
}

if ($credentials.CachedAccountName) {
    Write-Host "Using cached account '$($credentials.CachedAccountName)' ($($credentials.AuthType)) for integration tests." -ForegroundColor Cyan
}

$testPath = Resolve-Path -Path $Path -ErrorAction Stop
Write-Host "Running Pester against $testPath..." -ForegroundColor Cyan

$container = New-PesterContainer -Path $testPath -Data $pesterData
$result = Invoke-Pester -Container $container -Output Detailed -PassThru

if ($PassThru) {
    return $result
}

if ($result.FailedCount -gt 0) {
    exit $result.FailedCount
}

exit 0