/**
 * KB query security regression — S4-001 (sql.raw UUID array injection).
 */
import { describe, it, expect, vi } from 'vitest'

type SqlChunk = { value?: string[]; queryChunks?: unknown[] }

function collectSqlTextAndParams(
  node: unknown,
  text: string[],
  params: unknown[],
  seen = new Set<unknown>(),
): void {
  if (node == null || typeof node !== 'object' || seen.has(node)) return
  seen.add(node)

  if (Array.isArray(node)) {
    params.push(...node)
    return
  }

  if (typeof node === 'string' || typeof node === 'number' || typeof node === 'boolean') {
    params.push(node)
    return
  }

  const chunk = node as SqlChunk
  if (Array.isArray(chunk.queryChunks)) {
    for (const child of chunk.queryChunks) {
      collectSqlTextAndParams(child, text, params, seen)
    }
  }
  if (Array.isArray(chunk.value)) {
    text.push(chunk.value.join(''))
  } else if ('value' in chunk && !('queryChunks' in chunk)) {
    params.push(chunk.value)
  }
}

describe('listArticlesBySpaceIds (S4-001 regression)', () => {
  it('parameterizes space IDs — malicious payload never appears as raw SQL', async () => {
    const malicious = "'); SELECT pg_sleep(1); --"
    let capturedWhere: unknown

    const db = {
      select: vi.fn(() => ({
        from: vi.fn(() => ({
          where: vi.fn((cond: unknown) => {
            capturedWhere = cond
            return Promise.resolve([])
          }),
        })),
      })),
    }

    const { kbTenantQuery } = await import('../../src/queries/kb')
    const tenantId = '00000000-0000-4000-8000-000000000001'
    const result = await kbTenantQuery(db as never, tenantId).listArticlesBySpaceIds([malicious])

    expect(result).toEqual([])
    expect(capturedWhere).toBeDefined()

    const text: string[] = []
    const params: unknown[] = []
    collectSqlTextAndParams(capturedWhere, text, params)

    const sqlText = text.join('')
    const boundValues = params.map((p) =>
      p && typeof p === 'object' && 'value' in p
        ? (p as { value: unknown }).value
        : p,
    )

    // Old sql.raw path inlined user input into executable SQL text.
    expect(sqlText).not.toContain(malicious)
    expect(sqlText).not.toMatch(/SELECT pg_sleep/)
    // Drizzle inArray uses a bound parameter list, not string-built ARRAY literals.
    expect(sqlText).toMatch(/ in /)
    expect(boundValues).toContain(malicious)
  })
})
