Tests/AD-LinuxInventory.Tests.ps1

BeforeAll {
    $modulePath = Split-Path -Parent $PSScriptRoot
    Import-Module "$modulePath\AD-LinuxInventory.psd1" -Force
}

Describe 'AD-LinuxInventory Module' {

    Context 'Module Loading' {
        It 'Should import without errors' {
            { Import-Module "$PSScriptRoot\..\AD-LinuxInventory.psd1" -Force } | Should -Not -Throw
        }

        It 'Should export exactly 7 public functions' {
            $commands = Get-Command -Module AD-LinuxInventory
            $commands.Count | Should -Be 7
        }

        It 'Should export all expected functions' {
            $expected = @(
                'Register-LinuxServer',
                'Import-LinuxInventory',
                'Get-LinuxInventory',
                'Update-LinuxServer',
                'Sync-LinuxInventory',
                'Import-AgentRegistration',
                'Get-RegistrationHeartbeat'
            )
            foreach ($func in $expected) {
                Get-Command -Module AD-LinuxInventory -Name $func | Should -Not -BeNullOrEmpty
            }
        }

        It 'Should not export private functions' {
            { Get-Command -Module AD-LinuxInventory -Name Initialize-LinuxOU -ErrorAction Stop } | Should -Throw
            { Get-Command -Module AD-LinuxInventory -Name New-HtmlDashboard -ErrorAction Stop } | Should -Throw
        }
    }

    Context 'Register-LinuxServer Parameter Validation' {
        It 'Should have mandatory Name parameter' {
            (Get-Command Register-LinuxServer).Parameters['Name'].Attributes.Mandatory | Should -Contain $true
        }

        It 'Should have mandatory IPAddress parameter' {
            (Get-Command Register-LinuxServer).Parameters['IPAddress'].Attributes.Mandatory | Should -Contain $true
        }

        It 'Should default OperatingSystem to Linux' {
            $default = (Get-Command Register-LinuxServer).Parameters['OperatingSystem'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.PSDefaultValueAttribute] -or $_.TypeId.Name -eq 'ParameterAttribute' }
            # The default is set in code, not via attribute -- verify the param exists
            (Get-Command Register-LinuxServer).Parameters.ContainsKey('OperatingSystem') | Should -BeTrue
        }

        It 'Should support -WhatIf' {
            (Get-Command Register-LinuxServer).Parameters.ContainsKey('WhatIf') | Should -BeTrue
        }

        It 'Should support -Confirm' {
            (Get-Command Register-LinuxServer).Parameters.ContainsKey('Confirm') | Should -BeTrue
        }

        It 'Should have Force switch' {
            (Get-Command Register-LinuxServer).Parameters['Force'].SwitchParameter | Should -BeTrue
        }

        It 'Should validate IPAddress format' {
            $validatePattern = (Get-Command Register-LinuxServer).Parameters['IPAddress'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ValidatePatternAttribute] }
            $validatePattern | Should -Not -BeNullOrEmpty
        }
    }

    Context 'Import-LinuxInventory Parameter Validation' {
        It 'Should have mandatory CsvPath parameter' {
            (Get-Command Import-LinuxInventory).Parameters['CsvPath'].Attributes.Mandatory | Should -Contain $true
        }

        It 'Should validate CsvPath with ValidateScript' {
            $validateScript = (Get-Command Import-LinuxInventory).Parameters['CsvPath'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ValidateScriptAttribute] }
            $validateScript | Should -Not -BeNullOrEmpty
        }

        It 'Should have Force switch' {
            (Get-Command Import-LinuxInventory).Parameters['Force'].SwitchParameter | Should -BeTrue
        }
    }

    Context 'Get-LinuxInventory Parameter Validation' {
        It 'Should have Online switch' {
            (Get-Command Get-LinuxInventory).Parameters['Online'].SwitchParameter | Should -BeTrue
        }

        It 'Should accept OutputPath parameter' {
            (Get-Command Get-LinuxInventory).Parameters.ContainsKey('OutputPath') | Should -BeTrue
        }

        It 'Should accept OrganizationalUnit parameter' {
            (Get-Command Get-LinuxInventory).Parameters.ContainsKey('OrganizationalUnit') | Should -BeTrue
        }
    }

    Context 'Update-LinuxServer Parameter Validation' {
        It 'Should have mandatory Name parameter' {
            (Get-Command Update-LinuxServer).Parameters['Name'].Attributes.Mandatory | Should -Contain $true
        }

        It 'Should accept pipeline input for Name' {
            (Get-Command Update-LinuxServer).Parameters['Name'].Attributes.ValueFromPipeline | Should -Contain $true
        }

        It 'Should support -WhatIf' {
            (Get-Command Update-LinuxServer).Parameters.ContainsKey('WhatIf') | Should -BeTrue
        }

        It 'Should validate IPAddress format' {
            $validatePattern = (Get-Command Update-LinuxServer).Parameters['IPAddress'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ValidatePatternAttribute] }
            $validatePattern | Should -Not -BeNullOrEmpty
        }
    }

    Context 'Sync-LinuxInventory Parameter Validation' {
        It 'Should have UpdateDescription switch' {
            (Get-Command Sync-LinuxInventory).Parameters['UpdateDescription'].SwitchParameter | Should -BeTrue
        }

        It 'Should have TimeoutMs parameter with default 1000' {
            (Get-Command Sync-LinuxInventory).Parameters.ContainsKey('TimeoutMs') | Should -BeTrue
        }

        It 'Should accept OrganizationalUnit parameter' {
            (Get-Command Sync-LinuxInventory).Parameters.ContainsKey('OrganizationalUnit') | Should -BeTrue
        }
    }

    Context 'Import-AgentRegistration Parameter Validation' {
        It 'Should have mandatory RegistrationPath parameter' {
            (Get-Command Import-AgentRegistration).Parameters['RegistrationPath'].Attributes.Mandatory | Should -Contain $true
        }

        It 'Should validate RegistrationPath with ValidateScript' {
            $validateScript = (Get-Command Import-AgentRegistration).Parameters['RegistrationPath'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ValidateScriptAttribute] }
            $validateScript | Should -Not -BeNullOrEmpty
        }

        It 'Should have Token parameter' {
            (Get-Command Import-AgentRegistration).Parameters.ContainsKey('Token') | Should -BeTrue
        }

        It 'Should have OrganizationalUnit parameter' {
            (Get-Command Import-AgentRegistration).Parameters.ContainsKey('OrganizationalUnit') | Should -BeTrue
        }

        It 'Should have AutoApprove switch' {
            (Get-Command Import-AgentRegistration).Parameters['AutoApprove'].SwitchParameter | Should -BeTrue
        }

        It 'Should have OutputPath parameter' {
            (Get-Command Import-AgentRegistration).Parameters.ContainsKey('OutputPath') | Should -BeTrue
        }

        It 'Should support -WhatIf' {
            (Get-Command Import-AgentRegistration).Parameters.ContainsKey('WhatIf') | Should -BeTrue
        }

        It 'Should support -Confirm' {
            (Get-Command Import-AgentRegistration).Parameters.ContainsKey('Confirm') | Should -BeTrue
        }

        It 'Should have SupportsShouldProcess attribute' {
            $cmdInfo = Get-Command Import-AgentRegistration
            $cmdletBinding = $cmdInfo.ScriptBlock.Attributes | Where-Object { $_ -is [System.Management.Automation.CmdletBindingAttribute] }
            $cmdletBinding.SupportsShouldProcess | Should -BeTrue
        }
    }

    Context 'Get-RegistrationHeartbeat Parameter Validation' {
        It 'Should have RegistrationPath parameter' {
            (Get-Command Get-RegistrationHeartbeat).Parameters.ContainsKey('RegistrationPath') | Should -BeTrue
        }

        It 'Should have OrganizationalUnit parameter' {
            (Get-Command Get-RegistrationHeartbeat).Parameters.ContainsKey('OrganizationalUnit') | Should -BeTrue
        }

        It 'Should have StaleHours parameter with default 2' {
            (Get-Command Get-RegistrationHeartbeat).Parameters.ContainsKey('StaleHours') | Should -BeTrue
        }

        It 'Should have OfflineHours parameter with default 24' {
            (Get-Command Get-RegistrationHeartbeat).Parameters.ContainsKey('OfflineHours') | Should -BeTrue
        }

        It 'Should have OutputPath parameter' {
            (Get-Command Get-RegistrationHeartbeat).Parameters.ContainsKey('OutputPath') | Should -BeTrue
        }

        It 'Should validate StaleHours range' {
            $validateRange = (Get-Command Get-RegistrationHeartbeat).Parameters['StaleHours'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
            $validateRange | Should -Not -BeNullOrEmpty
        }

        It 'Should validate OfflineHours range' {
            $validateRange = (Get-Command Get-RegistrationHeartbeat).Parameters['OfflineHours'].Attributes |
                Where-Object { $_ -is [System.Management.Automation.ValidateRangeAttribute] }
            $validateRange | Should -Not -BeNullOrEmpty
        }
    }

    Context 'Register-LinuxServer Mocked Execution' {
        BeforeAll {
            Mock -ModuleName AD-LinuxInventory Import-Module { }
            Mock -ModuleName AD-LinuxInventory Get-ADDomain {
                [PSCustomObject]@{
                    DistinguishedName = 'DC=contoso,DC=com'
                    DNSRoot           = 'contoso.com'
                }
            }
            Mock -ModuleName AD-LinuxInventory Get-ADOrganizationalUnit { $true }
            Mock -ModuleName AD-LinuxInventory New-ADOrganizationalUnit { }
            Mock -ModuleName AD-LinuxInventory Get-ADComputer { throw [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]::new('Not found') }
            Mock -ModuleName AD-LinuxInventory New-ADComputer { }
            Mock -ModuleName AD-LinuxInventory Set-ADComputer { }
            Mock -ModuleName AD-LinuxInventory Remove-ADComputer { }
        }

        It 'Should call New-ADComputer with correct name' {
            Register-LinuxServer -Name 'test-srv-01' -IPAddress '10.0.0.1' -OperatingSystem 'Ubuntu 22.04'
            Should -Invoke -ModuleName AD-LinuxInventory -CommandName New-ADComputer -Times 1 -ParameterFilter {
                $Name -eq 'TEST-SRV-01'
            }
        }

        It 'Should call Set-ADComputer to set OS and IP properties' {
            Register-LinuxServer -Name 'test-srv-02' -IPAddress '10.0.0.2' -OperatingSystem 'RHEL 9'
            Should -Invoke -ModuleName AD-LinuxInventory -CommandName Set-ADComputer -Times 1
        }

        It 'Should return an object with Registered status' {
            $result = Register-LinuxServer -Name 'test-srv-03' -IPAddress '10.0.0.3'
            $result.Status | Should -Be 'Registered'
            $result.Name | Should -Be 'TEST-SRV-03'
            $result.IPv4Address | Should -Be '10.0.0.3'
        }

        It 'Should set ManagedBy when specified' {
            Register-LinuxServer -Name 'test-srv-04' -IPAddress '10.0.0.4' -ManagedBy 'jsmith'
            Should -Invoke -ModuleName AD-LinuxInventory -CommandName Set-ADComputer -Times 1 -ParameterFilter {
                $ManagedBy -eq 'jsmith'
            }
        }
    }

    Context 'Get-LinuxInventory Mocked Execution' {
        BeforeAll {
            Mock -ModuleName AD-LinuxInventory Import-Module { }
            Mock -ModuleName AD-LinuxInventory Get-ADDomain {
                [PSCustomObject]@{
                    DistinguishedName = 'DC=contoso,DC=com'
                    DNSRoot           = 'contoso.com'
                }
            }
            Mock -ModuleName AD-LinuxInventory Get-ADOrganizationalUnit { $true }
            Mock -ModuleName AD-LinuxInventory Get-ADComputer {
                @(
                    [PSCustomObject]@{
                        Name                   = 'WEB-PROD-01'
                        DNSHostName            = 'web-prod-01.contoso.com'
                        IPv4Address            = '10.1.2.50'
                        OperatingSystem        = 'Ubuntu 22.04 LTS'
                        OperatingSystemVersion = '22.04'
                        Description            = 'Production web server'
                        ManagedBy              = 'CN=John Smith,OU=Users,DC=contoso,DC=com'
                        Created                = (Get-Date).AddMonths(-6)
                        Modified               = (Get-Date).AddDays(-1)
                        Enabled                = $true
                    },
                    [PSCustomObject]@{
                        Name                   = 'DB-PROD-01'
                        DNSHostName            = 'db-prod-01.contoso.com'
                        IPv4Address            = '10.1.3.10'
                        OperatingSystem        = 'RHEL 9'
                        OperatingSystemVersion = '9.3'
                        Description            = 'Production database'
                        ManagedBy              = $null
                        Created                = (Get-Date).AddMonths(-3)
                        Modified               = (Get-Date).AddDays(-7)
                        Enabled                = $true
                    }
                )
            }
        }

        It 'Should return all servers from the OU' {
            $results = Get-LinuxInventory
            @($results).Count | Should -Be 2
        }

        It 'Should include expected properties on output objects' {
            $results = Get-LinuxInventory
            $first = $results[0]
            $first.Name | Should -Be 'WEB-PROD-01'
            $first.IPv4Address | Should -Be '10.1.2.50'
            $first.OperatingSystem | Should -Be 'Ubuntu 22.04 LTS'
            $first.Description | Should -Be 'Production web server'
        }

        It 'Should have Online property as null when -Online is not specified' {
            $results = Get-LinuxInventory
            $results[0].Online | Should -BeNullOrEmpty
        }
    }

    Context 'Sync-LinuxInventory Mocked Execution' {
        BeforeAll {
            Mock -ModuleName AD-LinuxInventory Import-Module { }
            Mock -ModuleName AD-LinuxInventory Get-ADDomain {
                [PSCustomObject]@{
                    DistinguishedName = 'DC=contoso,DC=com'
                    DNSRoot           = 'contoso.com'
                }
            }
            Mock -ModuleName AD-LinuxInventory Get-ADOrganizationalUnit { $true }
            Mock -ModuleName AD-LinuxInventory Set-ADComputer { }
            Mock -ModuleName AD-LinuxInventory Get-ADComputer {
                @(
                    [PSCustomObject]@{
                        Name            = 'WEB-PROD-01'
                        DNSHostName     = 'web-prod-01.contoso.com'
                        IPv4Address     = '10.1.2.50'
                        Description     = 'Production web server'
                        OperatingSystem = 'Ubuntu 22.04 LTS'
                    },
                    [PSCustomObject]@{
                        Name            = 'DB-OFFLINE-01'
                        DNSHostName     = 'db-offline-01.contoso.com'
                        IPv4Address     = '10.1.3.99'
                        Description     = 'Decommissioned DB'
                        OperatingSystem = 'CentOS 7'
                    }
                )
            }
            Mock -ModuleName AD-LinuxInventory Test-Connection {
                param($ComputerName)
                if ($ComputerName -eq '10.1.2.50') {
                    [PSCustomObject]@{ ResponseTime = 2 }
                }
                else {
                    $null
                }
            }
        }

        It 'Should return results for all servers' {
            $results = Sync-LinuxInventory
            @($results).Count | Should -Be 2
        }

        It 'Should detect online server correctly' {
            $results = Sync-LinuxInventory
            $online = $results | Where-Object { $_.Name -eq 'WEB-PROD-01' }
            $online.Online | Should -BeTrue
        }

        It 'Should detect offline server correctly' {
            $results = Sync-LinuxInventory
            $offline = $results | Where-Object { $_.Name -eq 'DB-OFFLINE-01' }
            $offline.Online | Should -BeFalse
        }

        It 'Should include IPAddress in results' {
            $results = Sync-LinuxInventory
            $results[0].IPAddress | Should -Be '10.1.2.50'
        }
    }

    Context 'Import-AgentRegistration Mocked Execution' {
        BeforeAll {
            # Create a temp directory with sample JSON files
            $script:tempRegDir = Join-Path -Path $env:TEMP -ChildPath "ad-reg-test-$(Get-Random)"
            New-Item -Path $script:tempRegDir -ItemType Directory -Force | Out-Null

            # Sample registration JSON (Ubuntu)
            $ubuntuJson = @{
                hostname                 = 'web-linux-01'
                fqdn                     = 'web-linux-01.contoso.com'
                ip_addresses             = @('10.1.2.50', '10.1.2.51')
                operating_system         = 'Ubuntu 24.04.1 LTS'
                operating_system_version = '24.04.1'
                kernel                   = '6.8.0-41-generic'
                os_family                = 'Linux'
                cpu_cores                = 8
                memory_gb                = 32
                disk_gb                  = 500
                uptime_hours             = 720
                services                 = @('nginx', 'postgresql', 'ssh')
                package_count            = 423
                agent_version            = '2.0.0'
                registered_at            = '2026-02-17T10:30:00Z'
                token                    = 'test-secret'
            } | ConvertTo-Json
            $ubuntuJson | Out-File -FilePath (Join-Path $script:tempRegDir 'web-linux-01.json') -Encoding UTF8

            # Sample registration JSON (macOS)
            $macJson = @{
                hostname                 = 'macbook-dev-01'
                fqdn                     = 'macbook-dev-01.contoso.com'
                ip_addresses             = @('10.1.4.30')
                operating_system         = 'macOS 15.2 Sequoia'
                operating_system_version = '15.2'
                kernel                   = '24.2.0'
                os_family                = 'macOS'
                cpu_cores                = 10
                memory_gb                = 16
                disk_gb                  = 512
                uptime_hours             = 48
                services                 = @('com.apple.Finder', 'com.apple.dock')
                package_count            = 85
                agent_version            = '2.0.0'
                registered_at            = '2026-02-17T10:30:00Z'
                token                    = 'test-secret'
            } | ConvertTo-Json
            $macJson | Out-File -FilePath (Join-Path $script:tempRegDir 'macbook-dev-01.json') -Encoding UTF8

            # Bad token JSON
            $badTokenJson = @{
                hostname                 = 'rogue-server'
                fqdn                     = 'rogue.contoso.com'
                ip_addresses             = @('10.99.99.1')
                operating_system         = 'Kali Linux 2024.1'
                operating_system_version = '2024.1'
                os_family                = 'Linux'
                token                    = 'wrong-token'
            } | ConvertTo-Json
            $badTokenJson | Out-File -FilePath (Join-Path $script:tempRegDir 'rogue-server.json') -Encoding UTF8

            Mock -ModuleName AD-LinuxInventory Import-Module { }
            Mock -ModuleName AD-LinuxInventory Get-ADDomain {
                [PSCustomObject]@{
                    DistinguishedName = 'DC=contoso,DC=com'
                    DNSRoot           = 'contoso.com'
                }
            }
            Mock -ModuleName AD-LinuxInventory Get-ADOrganizationalUnit { $true }
            Mock -ModuleName AD-LinuxInventory New-ADOrganizationalUnit { }
            Mock -ModuleName AD-LinuxInventory New-ADComputer { }
            Mock -ModuleName AD-LinuxInventory Set-ADComputer { }
            Mock -ModuleName AD-LinuxInventory Get-ADComputer {
                param($Identity)
                throw [Microsoft.ActiveDirectory.Management.ADIdentityNotFoundException]::new("Not found: $Identity")
            }
        }

        AfterAll {
            if (Test-Path $script:tempRegDir) {
                Remove-Item -Path $script:tempRegDir -Recurse -Force -ErrorAction SilentlyContinue
            }
        }

        It 'Should process JSON files and return summary' {
            $result = Import-AgentRegistration -RegistrationPath $script:tempRegDir -Token 'test-secret' -AutoApprove
            $result | Should -Not -BeNullOrEmpty
            $result.Processed | Should -BeGreaterOrEqual 1
        }

        It 'Should reject files with wrong token' {
            # Re-create the bad-token file since previous test may have moved it
            $badTokenJson = @{
                hostname = 'rogue-server'; fqdn = 'rogue.contoso.com'; ip_addresses = @('10.99.99.1')
                operating_system = 'Kali Linux'; operating_system_version = '2024.1'; os_family = 'Linux'; token = 'wrong-token'
            } | ConvertTo-Json
            $badTokenJson | Out-File -FilePath (Join-Path $script:tempRegDir 'rogue-server.json') -Encoding UTF8

            $result = Import-AgentRegistration -RegistrationPath $script:tempRegDir -Token 'test-secret' -AutoApprove
            $result.Rejected | Should -BeGreaterOrEqual 1
        }

        It 'Should call New-ADComputer for new registrations' {
            # Re-create a test file
            $testJson = @{
                hostname = 'new-srv-01'; fqdn = 'new-srv-01.contoso.com'; ip_addresses = @('10.0.0.10')
                operating_system = 'Debian 12'; operating_system_version = '12'; os_family = 'Linux'; token = 'test-secret'
            } | ConvertTo-Json
            $testJson | Out-File -FilePath (Join-Path $script:tempRegDir 'new-srv-01.json') -Encoding UTF8

            Import-AgentRegistration -RegistrationPath $script:tempRegDir -Token 'test-secret' -AutoApprove
            Should -Invoke -ModuleName AD-LinuxInventory -CommandName New-ADComputer -Times 1 -ParameterFilter {
                $Name -eq 'NEW-SRV-01'
            }
        }

        It 'Should call Set-ADComputer to set OS properties on new registrations' {
            $testJson = @{
                hostname = 'new-srv-02'; fqdn = 'new-srv-02.contoso.com'; ip_addresses = @('10.0.0.11')
                operating_system = 'FreeBSD 14.1-RELEASE'; operating_system_version = '14.1'; os_family = 'FreeBSD'; token = 'test-secret'
            } | ConvertTo-Json
            $testJson | Out-File -FilePath (Join-Path $script:tempRegDir 'new-srv-02.json') -Encoding UTF8

            Import-AgentRegistration -RegistrationPath $script:tempRegDir -Token 'test-secret' -AutoApprove
            Should -Invoke -ModuleName AD-LinuxInventory -CommandName Set-ADComputer -Times 1
        }

        It 'Should archive processed files to processed subfolder' {
            $testJson = @{
                hostname = 'archive-test'; fqdn = 'archive-test.contoso.com'; ip_addresses = @('10.0.0.20')
                operating_system = 'Alpine Linux 3.19'; operating_system_version = '3.19'; os_family = 'Linux'; token = 'test-secret'
            } | ConvertTo-Json
            $testJson | Out-File -FilePath (Join-Path $script:tempRegDir 'archive-test.json') -Encoding UTF8

            Import-AgentRegistration -RegistrationPath $script:tempRegDir -Token 'test-secret' -AutoApprove

            $processedDir = Join-Path $script:tempRegDir 'processed'
            $archivedFiles = Get-ChildItem -Path $processedDir -Filter '*archive-test*' -ErrorAction SilentlyContinue
            $archivedFiles | Should -Not -BeNullOrEmpty
        }
    }

    Context 'Get-RegistrationHeartbeat Mocked Execution' {
        BeforeAll {
            # Create a temp directory with sample JSON files
            $script:tempHbDir = Join-Path -Path $env:TEMP -ChildPath "ad-hb-test-$(Get-Random)"
            New-Item -Path $script:tempHbDir -ItemType Directory -Force | Out-Null

            # Create a "recent" JSON file
            '{}' | Out-File -FilePath (Join-Path $script:tempHbDir 'ACTIVE-SRV-01.json') -Encoding UTF8

            # Create an "old" JSON file (stale)
            $staleFile = Join-Path $script:tempHbDir 'STALE-SRV-01.json'
            '{}' | Out-File -FilePath $staleFile -Encoding UTF8
            (Get-Item $staleFile).LastWriteTime = (Get-Date).AddHours(-5)

            # Create a very old JSON file (offline)
            $offlineFile = Join-Path $script:tempHbDir 'OFFLINE-SRV-01.json'
            '{}' | Out-File -FilePath $offlineFile -Encoding UTF8
            (Get-Item $offlineFile).LastWriteTime = (Get-Date).AddHours(-48)

            Mock -ModuleName AD-LinuxInventory Import-Module { }
            Mock -ModuleName AD-LinuxInventory Get-ADDomain {
                [PSCustomObject]@{
                    DistinguishedName = 'DC=contoso,DC=com'
                    DNSRoot           = 'contoso.com'
                }
            }
            Mock -ModuleName AD-LinuxInventory Get-ADOrganizationalUnit { $true }
            Mock -ModuleName AD-LinuxInventory Get-ADComputer {
                @(
                    [PSCustomObject]@{
                        Name                   = 'ACTIVE-SRV-01'
                        DNSHostName            = 'active-srv-01.contoso.com'
                        IPv4Address            = '10.1.1.1'
                        OperatingSystem        = 'Ubuntu 24.04.1 LTS'
                        OperatingSystemVersion = '24.04.1'
                        Description            = 'Linux | 4 cores | 8GB RAM'
                        Created                = (Get-Date).AddMonths(-1)
                        Modified               = (Get-Date).AddMinutes(-30)
                    },
                    [PSCustomObject]@{
                        Name                   = 'STALE-SRV-01'
                        DNSHostName            = 'stale-srv-01.contoso.com'
                        IPv4Address            = '10.1.1.2'
                        OperatingSystem        = 'macOS 15.2 Sequoia'
                        OperatingSystemVersion = '15.2'
                        Description            = 'macOS | 10 cores | 16GB RAM'
                        Created                = (Get-Date).AddMonths(-2)
                        Modified               = (Get-Date).AddHours(-5)
                    },
                    [PSCustomObject]@{
                        Name                   = 'OFFLINE-SRV-01'
                        DNSHostName            = 'offline-srv-01.contoso.com'
                        IPv4Address            = '10.1.1.3'
                        OperatingSystem        = 'FreeBSD 14.1-RELEASE'
                        OperatingSystemVersion = '14.1'
                        Description            = 'FreeBSD | 2 cores | 4GB RAM'
                        Created                = (Get-Date).AddMonths(-6)
                        Modified               = (Get-Date).AddHours(-48)
                    },
                    [PSCustomObject]@{
                        Name                   = 'MANUAL-SRV-01'
                        DNSHostName            = 'manual-srv-01.contoso.com'
                        IPv4Address            = '10.1.1.4'
                        OperatingSystem        = 'RHEL 9'
                        OperatingSystemVersion = '9.3'
                        Description            = 'Manually registered'
                        Created                = (Get-Date).AddMonths(-3)
                        Modified               = (Get-Date).AddMonths(-3)
                    }
                )
            }
        }

        AfterAll {
            if (Test-Path $script:tempHbDir) {
                Remove-Item -Path $script:tempHbDir -Recurse -Force -ErrorAction SilentlyContinue
            }
        }

        It 'Should return all systems from the OU' {
            $results = Get-RegistrationHeartbeat -RegistrationPath $script:tempHbDir
            @($results).Count | Should -Be 4
        }

        It 'Should classify recent heartbeat as Active' {
            $results = Get-RegistrationHeartbeat -RegistrationPath $script:tempHbDir
            $active = $results | Where-Object { $_.ComputerName -eq 'ACTIVE-SRV-01' }
            $active.Status | Should -Be 'Active'
        }

        It 'Should classify old heartbeat as Stale' {
            $results = Get-RegistrationHeartbeat -RegistrationPath $script:tempHbDir -StaleHours 2 -OfflineHours 24
            $stale = $results | Where-Object { $_.ComputerName -eq 'STALE-SRV-01' }
            $stale.Status | Should -Be 'Stale'
        }

        It 'Should classify very old heartbeat as Offline' {
            $results = Get-RegistrationHeartbeat -RegistrationPath $script:tempHbDir -StaleHours 2 -OfflineHours 24
            $offline = $results | Where-Object { $_.ComputerName -eq 'OFFLINE-SRV-01' }
            $offline.Status | Should -Be 'Offline'
        }

        It 'Should classify systems without JSON file as Never' {
            $results = Get-RegistrationHeartbeat -RegistrationPath $script:tempHbDir
            $never = $results | Where-Object { $_.ComputerName -eq 'MANUAL-SRV-01' }
            $never.Status | Should -Be 'Never'
        }

        It 'Should include HoursSinceHeartbeat for systems with JSON files' {
            $results = Get-RegistrationHeartbeat -RegistrationPath $script:tempHbDir
            $active = $results | Where-Object { $_.ComputerName -eq 'ACTIVE-SRV-01' }
            $active.HoursSinceHeartbeat | Should -Not -BeNullOrEmpty
        }

        It 'Should have null HoursSinceHeartbeat for Never status' {
            $results = Get-RegistrationHeartbeat -RegistrationPath $script:tempHbDir
            $never = $results | Where-Object { $_.ComputerName -eq 'MANUAL-SRV-01' }
            $never.HoursSinceHeartbeat | Should -BeNullOrEmpty
        }
    }

    Context 'Manifest Validation' {
        BeforeAll {
            $manifest = Test-ModuleManifest -Path "$PSScriptRoot\..\AD-LinuxInventory.psd1"
        }

        It 'Should have version 2.0.0' {
            $manifest.Version.ToString() | Should -Be '2.0.0'
        }

        It 'Should have correct author' {
            $manifest.Author | Should -Be 'Larry Roberts'
        }

        It 'Should have correct GUID' {
            $manifest.GUID.ToString() | Should -Be 'd2e8f3a1-5b49-4c7d-ae12-9f3c6d8b7e45'
        }

        It 'Should have ProjectUri pointing to GitHub' {
            $manifest.PrivateData.PSData.ProjectUri | Should -Be 'https://github.com/larro1991/AD-LinuxInventory'
        }

        It 'Should have LicenseUri pointing to GitHub' {
            $manifest.PrivateData.PSData.LicenseUri | Should -Be 'https://github.com/larro1991/AD-LinuxInventory/blob/master/LICENSE'
        }

        It 'Should include original tags' {
            $tags = $manifest.PrivateData.PSData.Tags
            $tags | Should -Contain 'ActiveDirectory'
            $tags | Should -Contain 'Linux'
            $tags | Should -Contain 'Inventory'
            $tags | Should -Contain 'CrossPlatform'
            $tags | Should -Contain 'ServerManagement'
        }

        It 'Should include new tags for v2.0' {
            $tags = $manifest.PrivateData.PSData.Tags
            $tags | Should -Contain 'macOS'
            $tags | Should -Contain 'FreeBSD'
            $tags | Should -Contain 'Unix'
            $tags | Should -Contain 'SelfRegistration'
            $tags | Should -Contain 'Agent'
        }

        It 'Should require PowerShell 5.1' {
            $manifest.PowerShellVersion.ToString() | Should -Be '5.1'
        }

        It 'Should export exactly 7 functions in manifest' {
            $manifest.ExportedFunctions.Count | Should -Be 7
        }
    }
}