tests/ConvertTo-DFToolRecord.Tests.ps1

BeforeAll {
    . "$PSScriptRoot/TestSupport.ps1"
    foreach ($f in Get-DFTestModuleFile) { . $f }
}

Describe 'ConvertTo-DFToolRecord' {
    It 'gives a minimal record every field, with defaults' {
        $r = ConvertTo-DFToolRecord ('{ "name": "t", "executable": "t.exe" }' | ConvertFrom-Json)
        $r.name        | Should -Be 't'
        $r.executable  | Should -Be 't.exe'
        $r.type        | Should -Be 'exe'
        $r.description | Should -Be ''
        @($r.tags).Count      | Should -Be 0
        @($r.after).Count | Should -Be 0
        $r.prewarm     | Should -BeTrue
        foreach ($p in 'packages', 'xdg', 'env', 'aliases', 'picker', 'themeMap', 'settings') {
            $r.PSObject.Properties[$p] | Should -Not -BeNullOrEmpty -Because "$p must exist"
            $r.$p | Should -BeNullOrEmpty
        }
    }

    It 'keeps every value the record sets, including fields it does not know' {
        $raw = '{ "name": "t", "executable": "t.exe", "type": "module", "prewarm": false,
                  "tags": ["a"], "after": ["x"], "customField": 42 }'
 | ConvertFrom-Json
        $r = ConvertTo-DFToolRecord $raw
        $r.type | Should -Be 'module'
        $r.prewarm | Should -BeFalse
        $r.tags | Should -Be @('a')
        $r.after | Should -Be @('x')
        $r.customField | Should -Be 42
    }

    It 'defaults executableExclude to an empty array' {
        $r = ConvertTo-DFToolRecord ('{ "name": "t", "executable": "t.exe" }' | ConvertFrom-Json)
        $r.PSObject.Properties['executableExclude'] | Should -Not -BeNullOrEmpty
        @($r.executableExclude).Count | Should -Be 0
    }

    It 'defaults roles to an empty object' {
        $r = ConvertTo-DFToolRecord ('{ "name": "t", "executable": "t.exe" }' | ConvertFrom-Json)
        @($r.roles.PSObject.Properties).Count | Should -Be 0
    }

    It 'normalizes each role block: priority defaults to 0, optIn defaults to false, and aliases get the alias shape' {
        $raw = '{ "name": "t", "executable": "t.exe", "roles": {
                    "listing": { "priority": 20, "aliases": { "ls": { "command": "t" } } },
                    "grep": {}, "optional": { "optIn": true } } }'
 | ConvertFrom-Json
        $r = ConvertTo-DFToolRecord $raw
        $r.roles.listing.priority | Should -Be 20
        $r.roles.listing.aliases.ls.command | Should -Be 't'
        @($r.roles.listing.aliases.ls.args).Count | Should -Be 0
        $r.roles.grep.priority | Should -Be 0
        $r.roles.grep.optIn | Should -BeFalse
        $r.roles.optional.optIn | Should -BeTrue
        $r.roles.grep.aliases | Should -BeNullOrEmpty
        $r.roles.grep.env | Should -BeNullOrEmpty
    }

    It 'converts the legacy role string to a roles entry and drops role' {
        $r = ConvertTo-DFToolRecord ('{ "name": "t", "executable": "t.exe", "role": "listing" }' | ConvertFrom-Json)
        $r.roles.listing.priority | Should -Be 0
        $r.PSObject.Properties['role'] | Should -BeNullOrEmpty
    }

    It 'prefers an explicit roles entry over the legacy string for the same role' {
        $raw = '{ "name": "t", "executable": "t.exe", "role": "listing", "roles": { "listing": { "priority": 5 } } }' | ConvertFrom-Json
        (ConvertTo-DFToolRecord $raw).roles.listing.priority | Should -Be 5
    }

    It 'normalizes xdg so every xdg field exists' {
        $r = ConvertTo-DFToolRecord ('{ "name": "t", "executable": "t", "xdg": { "method": "env" } }' | ConvertFrom-Json)
        $r.xdg.method | Should -Be 'env'
        foreach ($p in 'vars', 'dirs', 'instructions', 'compliance') {
            $r.xdg.PSObject.Properties[$p] | Should -Not -BeNullOrEmpty -Because "xdg.$p must exist"
        }
    }

    It 'normalizes each alias to { command; args[] }, treating a missing args as none' {
        $raw = '{ "name": "t", "executable": "t", "aliases": {
                    "a": { "command": "x" },
                    "b": { "command": "y", "args": "--one" },
                    "c": { "command": "z", "args": ["-p", "q"] } } }'
 | ConvertFrom-Json
        $r = ConvertTo-DFToolRecord $raw
        $r.aliases.a.command | Should -Be 'x'
        ,$r.aliases.a.args | Should -BeOfType [object[]]
        @($r.aliases.a.args).Count | Should -Be 0
        @($r.aliases.b.args) | Should -Be @('--one')
        @($r.aliases.c.args) | Should -Be @('-p', 'q')
    }

    It 'defaults the declarative picker fields, and leaves a non-object picker alone' {
        $r = ConvertTo-DFToolRecord ('{ "name": "t", "executable": "t", "picker": { "function": "F", "list": "ls" } }' | ConvertFrom-Json)
        $r.picker.preview_window    | Should -Be 'right:60%'
        $r.picker.preview           | Should -Be ''
        $r.picker.header            | Should -Be ''
        $r.picker.ansi              | Should -BeFalse
        $r.picker.list_accepts_path | Should -BeFalse
        foreach ($p in 'alias', 'action', 'parse') {
            $r.picker.PSObject.Properties[$p] | Should -Not -BeNullOrEmpty -Because "picker.$p must exist"
        }
        (ConvertTo-DFToolRecord ('{ "name": "t", "executable": "t", "picker": "custom" }' | ConvertFrom-Json)).picker |
            Should -Be 'custom'
    }

    It 'lets every shipped record be read with plain property access under StrictMode' {
        $fields = 'name', 'executable', 'type', 'description', 'tags', 'packages', 'xdg', 'env',
                  'aliases', 'picker', 'after', 'requires', 'roles', 'themeMap', 'settings', 'prewarm', 'setup'
        foreach ($file in Get-ChildItem (Join-Path $PSScriptRoot '..' 'Tools') -Filter '*.json') {
            $r = ConvertTo-DFToolRecord (Get-Content $file.FullName -Raw | ConvertFrom-Json)
            {
                Set-StrictMode -Version Latest
                foreach ($f in $fields) { $null = $r.$f }
                if ($r.xdg) { $null = $r.xdg.method, $r.xdg.vars, $r.xdg.dirs, $r.xdg.instructions, $r.xdg.compliance }
                if ($r.setup) { $null = $r.setup.seed }
                if ($r.picker -is [pscustomobject]) { $null = $r.picker.function, $r.picker.list, $r.picker.alias, $r.picker.action, $r.picker.parse }
                if ($r.aliases) { foreach ($a in $r.aliases.PSObject.Properties) { $null = $a.Value.command, $a.Value.args } }
            } | Should -Not -Throw -Because $file.Name
        }
    }
}