import { sql } from 'drizzle-orm'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Querier } from '@platform-modules/db'
import { createPgliteClient } from '../../db/src/postgres/pglite.js'
import { settingsSchema, type SettingsSchema } from './settings-schema.js'
import {
  SETTINGS_KEYS,
  SettingsValidationError,
  createDbSettingsStore,
  settingsTableSql,
  type SettingsStore,
} from './settings.js'

async function db(): Promise<Querier<SettingsSchema>> {
  const handle = createPgliteClient({ schema: settingsSchema })
  await handle.execute(sql.raw(settingsTableSql()))
  return handle as unknown as Querier<SettingsSchema>
}

describe('settingsTableSql', () => {
  it('emits idempotent DDL for settings_kv', () => {
    const ddl = settingsTableSql()
    expect(ddl).toContain('settings_kv')
    expect(ddl).toContain('key')
    expect(ddl).toContain('jsonb')
    expect(ddl).toContain('IF NOT EXISTS')
  })
})

describe('SETTINGS_KEYS', () => {
  it('exposes well-known namespaced keys', () => {
    expect(SETTINGS_KEYS.siteName).toBe('site.name')
    expect(SETTINGS_KEYS.maintenance).toBe('site.maintenance')
  })
})

describe('createDbSettingsStore', () => {
  let store: SettingsStore

  beforeEach(async () => {
    store = createDbSettingsStore(await db())
  })

  it('set then get round-trips a typed value', async () => {
    await store.set('site.name', 'Acme')
    const got = await store.get<string>('site.name')
    expect(got).toBe('Acme')
  })

  it('get returns null for a missing key', async () => {
    expect(await store.get('missing')).toBeNull()
  })

  it('getMany returns only present keys', async () => {
    await store.set('a', 1)
    await store.set('b', 2)
    const many = await store.getMany(['a', 'c', 'b'])
    expect(many).toEqual({ a: 1, b: 2 })
    expect(many).not.toHaveProperty('c')
  })

  it('all returns every stored key-value pair', async () => {
    await store.set('x', 'one')
    await store.set('y', { nested: true })
    const all = await store.all()
    expect(all).toEqual({ x: 'one', y: { nested: true } })
  })

  it('remove deletes a key; get returns null afterward', async () => {
    await store.set('gone', true)
    await store.remove('gone')
    expect(await store.get('gone')).toBeNull()
  })

  it('rejects an over-256-char key on set', async () => {
    const long = 'k'.repeat(257)
    await expect(store.set(long, 1)).rejects.toBeInstanceOf(SettingsValidationError)
    await expect(store.set(long, 1)).rejects.toMatchObject({ field: 'key' })
  })

  it('rejects an over-256-char key on get and remove', async () => {
    const long = 'k'.repeat(257)
    await expect(store.get(long)).rejects.toBeInstanceOf(SettingsValidationError)
    await expect(store.remove(long)).rejects.toBeInstanceOf(SettingsValidationError)
  })

  it('rejects a non-serializable value with SettingsValidationError, not TypeError', async () => {
    const circular: Record<string, unknown> = {}
    circular.self = circular
    await expect(store.set('circular', circular)).rejects.toBeInstanceOf(SettingsValidationError)
    await expect(store.set('circular', circular)).rejects.toMatchObject({ field: 'value' })
    await expect(store.set('bigint', BigInt(1))).rejects.toBeInstanceOf(SettingsValidationError)
  })

  it('get returns null for a corrupt row without throwing', async () => {
    await store.set('good', { ok: true })
    await store.set('corrupt', { x: 1 })
    const orig = JSON.stringify
    vi.spyOn(JSON, 'stringify').mockImplementation((value) => {
      if (value && typeof value === 'object' && (value as { x?: number }).x === 1) {
        throw new TypeError('not serializable')
      }
      return orig(value)
    })
    expect(await store.get('corrupt')).toBeNull()
    expect(await store.get('good')).toEqual({ ok: true })
    vi.restoreAllMocks()
  })

  it('upserts on set — later value wins', async () => {
    await store.set('site.url', 'https://old.test')
    await store.set('site.url', 'https://new.test')
    expect(await store.get('site.url')).toBe('https://new.test')
  })

  // --- security-guard P2-c: prototype-pollution floor ---

  it('rejects prototype-pollution keys on every op', async () => {
    for (const k of ['__proto__', 'constructor', 'prototype']) {
      await expect(store.set(k, 1)).rejects.toBeInstanceOf(SettingsValidationError)
      await expect(store.get(k)).rejects.toBeInstanceOf(SettingsValidationError)
      await expect(store.remove(k)).rejects.toBeInstanceOf(SettingsValidationError)
      await expect(store.getMany([k])).rejects.toBeInstanceOf(SettingsValidationError)
    }
  })

  it('does not pollute Object.prototype when a __proto__ row exists out-of-band', async () => {
    const handle = await db()
    const s = createDbSettingsStore(handle)
    await s.set('legit', 1)
    // simulate an out-of-band write that bypassed validateKey
    await handle.execute(sql`INSERT INTO settings_kv (key, value) VALUES ('__proto__', '{"polluted":true}'::jsonb)`)
    const all = await s.all()
    expect(({} as Record<string, unknown>).polluted).toBeUndefined() // global prototype intact
    expect(Object.getPrototypeOf(all)).toBeNull() // null-proto result object
    expect(all.legit).toBe(1)
  })
})
