modules/Azure/Discovery/Tests/Unit/InvokeCIEMBatchInsert.Tests.ps1

BeforeAll {
    Remove-Module Devolutions.CIEM -Force -ErrorAction SilentlyContinue
    Import-Module (Join-Path $PSScriptRoot '..' '..' '..' '..' '..' 'Devolutions.CIEM.psd1')
}

Describe 'InvokeCIEMBatchInsert shared helper' {
    BeforeEach {
        InModuleScope Devolutions.CIEM {
            $script:batchCallLog = [System.Collections.Generic.List[object]]::new()
        }
    }

    It 'Function exists and is private (not exported)' {
        InModuleScope Devolutions.CIEM {
            Get-Command -Name InvokeCIEMBatchInsert -ErrorAction Stop | Should -Not -BeNullOrEmpty
        }
        (Get-Command -Module Devolutions.CIEM -Name InvokeCIEMBatchInsert -ErrorAction SilentlyContinue) | Should -BeNullOrEmpty
    }

    It 'Computes chunk size from column count to stay under 999 SQLite parameter cap' {
        InModuleScope Devolutions.CIEM {
            Mock Invoke-CIEMQuery {
                $script:batchCallLog.Add([pscustomobject]@{
                    Query = $Query
                    Parameters = $Parameters
                })
            }

            # 17 columns (matches azure_arm_resources). Max chunk under 999 = floor(999/17) = 58.
            $columns = 1..17 | ForEach-Object { "col$_" }
            $items = 1..60 | ForEach-Object {
                $row = [ordered]@{}
                for ($i = 1; $i -le 17; $i++) { $row["col$i"] = "v${_}_$i" }
                [pscustomobject]$row
            }

            InvokeCIEMBatchInsert -Table 'test_table' -Columns $columns -Items $items

            @($script:batchCallLog).Count | Should -Be 2
            @($script:batchCallLog[0].Parameters.Keys).Count | Should -BeLessOrEqual 999
            @($script:batchCallLog[0].Parameters.Keys).Count | Should -Be (58 * 17)
            @($script:batchCallLog[1].Parameters.Keys).Count | Should -Be (2 * 17)
        }
    }

    It 'Uses a single INSERT when all items fit in one chunk' {
        InModuleScope Devolutions.CIEM {
            Mock Invoke-CIEMQuery {
                $script:batchCallLog.Add([pscustomobject]@{ Query = $Query; Parameters = $Parameters })
            }

            $columns = @('a', 'b', 'c')
            $items = 1..10 | ForEach-Object { [pscustomobject]@{ a = "a$_"; b = "b$_"; c = "c$_" } }

            InvokeCIEMBatchInsert -Table 'small_table' -Columns $columns -Items $items

            @($script:batchCallLog).Count | Should -Be 1
            @($script:batchCallLog[0].Parameters.Keys).Count | Should -Be 30
            $script:batchCallLog[0].Query | Should -Match 'INSERT OR REPLACE INTO small_table'
        }
    }

    It 'No-ops cleanly when items is empty' {
        InModuleScope Devolutions.CIEM {
            Mock Invoke-CIEMQuery {
                $script:batchCallLog.Add($Parameters)
            }

            InvokeCIEMBatchInsert -Table 'empty_table' -Columns @('a', 'b') -Items @()

            @($script:batchCallLog).Count | Should -Be 0
        }
    }

    It 'Respects an explicit Connection by routing to Invoke-PSUSQLiteQuery' {
        InModuleScope Devolutions.CIEM {
            Mock Invoke-PSUSQLiteQuery {
                $script:batchCallLog.Add([pscustomobject]@{ Target = 'PSUSQLite'; Parameters = $Parameters })
            }
            Mock Invoke-CIEMQuery {
                $script:batchCallLog.Add([pscustomobject]@{ Target = 'CIEMQuery'; Parameters = $Parameters })
            }

            $fakeConnection = [pscustomobject]@{ Marker = 'stub' }
            $items = 1..3 | ForEach-Object { [pscustomobject]@{ a = "a$_"; b = "b$_" } }

            InvokeCIEMBatchInsert -Table 'conn_table' -Columns @('a', 'b') -Items $items -Connection $fakeConnection

            @($script:batchCallLog).Count | Should -Be 1
            $script:batchCallLog[0].Target | Should -Be 'PSUSQLite'
        }
    }

    It 'Has ErrorActionPreference = Stop and throws on downstream failure' {
        InModuleScope Devolutions.CIEM {
            Mock Invoke-CIEMQuery { throw 'database boom' }
            $items = @([pscustomobject]@{ a = 1; b = 2 })
            { InvokeCIEMBatchInsert -Table 't' -Columns @('a', 'b') -Items $items } | Should -Throw '*database boom*'
        }
    }

    It 'Reads values from both hashtables and PSCustomObjects via a unified lookup' {
        InModuleScope Devolutions.CIEM {
            Mock Invoke-CIEMQuery {
                $script:batchCallLog.Add($Parameters)
            }

            $mixedItems = @(
                [pscustomobject]@{ Id = 'obj-1'; Name = 'alpha' }
                @{ Id = 'ht-2'; Name = 'beta' }
                [ordered]@{ Id = 'ord-3'; Name = 'gamma' }
            )

            InvokeCIEMBatchInsert -Table 'mixed_table' -Columns @('id', 'name') -Items $mixedItems

            @($script:batchCallLog).Count | Should -Be 1
            $capturedParams = $script:batchCallLog[0]
            $capturedParams['id_1'] | Should -Be 'obj-1'
            $capturedParams['name_1'] | Should -Be 'alpha'
            $capturedParams['id_2'] | Should -Be 'ht-2'
            $capturedParams['name_2'] | Should -Be 'beta'
            $capturedParams['id_3'] | Should -Be 'ord-3'
            $capturedParams['name_3'] | Should -Be 'gamma'
        }
    }

    It 'Throws fail-fast on items of unknown types (neither IDictionary nor PSObject-backed)' {
        InModuleScope Devolutions.CIEM {
            Mock Invoke-CIEMQuery {
                $script:batchCallLog.Add($Parameters)
            }

            # A raw int isn't a dictionary; BCL primitives expose a PSObject wrapper with
            # no Id/Name properties, so the lookup finds nothing. A fail-fast helper should
            # throw instead of silently inserting NULLs.
            $badItems = @([int]42)

            { InvokeCIEMBatchInsert -Table 'bad_table' -Columns @('id', 'name') -Items $badItems } |
                Should -Throw '*unsupported item type*'

            @($script:batchCallLog).Count | Should -Be 0
        }
    }
}